diff --git a/.editorconfig b/.editorconfig index 134066baff..3499a1f7a6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,3 +12,14 @@ charset = utf-8 [*.{csproj,props}] indent_size = 2 + +[*.{cs,vb}] +dotnet_naming_rule.private_members_with_underscore.symbols = private_fields +dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore +dotnet_naming_rule.private_members_with_underscore.severity = suggestion + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +dotnet_naming_style.prefix_underscore.capitalization = camel_case +dotnet_naming_style.prefix_underscore.required_prefix = _ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 9c5e937589..935756deab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,11 @@ language: csharp -dist: trusty sudo: required services: - postgresql before_script: - psql -c 'create database JsonApiDotNetCoreExample;' -U postgres mono: none -dotnet: 2.1.300 # https://www.microsoft.com/net/download/linux +dotnet: 3.0.100 # https://www.microsoft.com/net/download/linux branches: only: - master diff --git a/Build.ps1 b/Build.ps1 index 0700d6f253..60006411a8 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -35,12 +35,6 @@ CheckLastExitCode dotnet test ./test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj CheckLastExitCode -dotnet test ./test/OperationsExampleTests/OperationsExampleTests.csproj -CheckLastExitCode - -dotnet test ./test/ResourceEntitySeparationExampleTests/ResourceEntitySeparationExampleTests.csproj -CheckLastExitCode - dotnet test ./test/DiscoveryTests/DiscoveryTests.csproj CheckLastExitCode diff --git a/Directory.Build.props b/Directory.Build.props index 0d034e0c5d..5014190440 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,30 +1,23 @@ - - netcoreapp2.0 - netstandard2.0 - - 2.1.0 - - 2.1.0 - 2.1.0 - 2.1.0 - - 2.1.0 - 2.1.0 - - 4.0.0 - 2.1.0 - + netcoreapp3.0 + netstandard2.1 + 3.* + 3.* + 3.* + 3.* + 3.* + 3.* + 4.1.1 + 3.0.1 4.5.0 - 15.7.2 - 2.3.1 - 22.1.2 - 4.8.3 + 16.3.0 + 2.4.1 + 28.4.1 + 4.13.1 - - + \ No newline at end of file diff --git a/JsonApiDotnetCore.sln b/JsonApiDotnetCore.sln index a0330ce005..ad130ce4b1 100644 --- a/JsonApiDotnetCore.sln +++ b/JsonApiDotnetCore.sln @@ -1,241 +1,195 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28606.126 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCore", "src\JsonApiDotNetCore\JsonApiDotNetCore.csproj", "{C0EC9E70-EB2E-436F-9D94-FA16FA774123}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCoreExample", "src\Examples\JsonApiDotNetCoreExample\JsonApiDotNetCoreExample.csproj", "{97EE048B-16C0-43F6-BDA9-4E762B2F579F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCoreExampleTests", "test\JsonApiDotNetCoreExampleTests\JsonApiDotNetCoreExampleTests.csproj", "{0B959765-40D2-43B5-87EE-FE2FEF9DBED5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C5B4D998-CECB-454D-9F32-085A897577BE}" - ProjectSection(SolutionItems) = preProject - .gitignore = .gitignore - .travis.yml = .travis.yml - appveyor.yml = appveyor.yml - Build.ps1 = Build.ps1 - build.sh = build.sh - Directory.Build.props = Directory.Build.props - README.md = README.md - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NoEntityFrameworkExample", "src\Examples\NoEntityFrameworkExample\NoEntityFrameworkExample.csproj", "{570165EC-62B5-4684-A139-8D2A30DD4475}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NoEntityFrameworkTests", "test\NoEntityFrameworkTests\NoEntityFrameworkTests.csproj", "{73DA578D-A63F-4956-83ED-6D7102E09140}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "test\UnitTests\UnitTests.csproj", "{6D4BD85A-A262-44C6-8572-FE3A30410BF3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{026FBC6C-AF76-4568-9B87-EC73457899FD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReportsExample", "src\Examples\ReportsExample\ReportsExample.csproj", "{FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{076E1AE4-FD25-4684-B826-CAAE37FEA0AA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "benchmarks\Benchmarks.csproj", "{1F604666-BB0F-413E-922D-9D37C6073285}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OperationsExample", "src\Examples\OperationsExample\OperationsExample.csproj", "{CF2C1EB6-8449-4B35-B8C7-F43D6D90632D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OperationsExampleTests", "test\OperationsExampleTests\OperationsExampleTests.csproj", "{9CD2C116-D133-4FE4-97DA-A9FEAFF045F1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ResourceEntitySeparationExample", "src\Examples\ResourceEntitySeparationExample\ResourceEntitySeparationExample.csproj", "{F4097194-9415-418A-AB4E-315C5D5466AF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ResourceEntitySeparationExampleTests", "test\ResourceEntitySeparationExampleTests\ResourceEntitySeparationExampleTests.csproj", "{6DFA30D7-1679-4333-9779-6FB678E48EF5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GettingStarted", "src\Examples\GettingStarted\GettingStarted.csproj", "{DF9BFD82-D937-4907-B0B4-64670417115F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscoveryTests", "test\DiscoveryTests\DiscoveryTests.csproj", "{09C0C8D8-B721-4955-8889-55CB149C3B5C}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Debug|x64.ActiveCfg = Debug|Any CPU - {C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Debug|x86.ActiveCfg = Debug|Any CPU - {C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Release|Any CPU.Build.0 = Release|Any CPU - {C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Release|x64.ActiveCfg = Release|Any CPU - {C0EC9E70-EB2E-436F-9D94-FA16FA774123}.Release|x86.ActiveCfg = Release|Any CPU - {97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Debug|x64.ActiveCfg = Debug|Any CPU - {97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Debug|x86.ActiveCfg = Debug|Any CPU - {97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Release|Any CPU.Build.0 = Release|Any CPU - {97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Release|x64.ActiveCfg = Release|Any CPU - {97EE048B-16C0-43F6-BDA9-4E762B2F579F}.Release|x86.ActiveCfg = Release|Any CPU - {0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Debug|x64.ActiveCfg = Debug|Any CPU - {0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Debug|x86.ActiveCfg = Debug|Any CPU - {0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Release|Any CPU.Build.0 = Release|Any CPU - {0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Release|x64.ActiveCfg = Release|Any CPU - {0B959765-40D2-43B5-87EE-FE2FEF9DBED5}.Release|x86.ActiveCfg = Release|Any CPU - {570165EC-62B5-4684-A139-8D2A30DD4475}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {570165EC-62B5-4684-A139-8D2A30DD4475}.Debug|Any CPU.Build.0 = Debug|Any CPU - {570165EC-62B5-4684-A139-8D2A30DD4475}.Debug|x64.ActiveCfg = Debug|Any CPU - {570165EC-62B5-4684-A139-8D2A30DD4475}.Debug|x64.Build.0 = Debug|Any CPU - {570165EC-62B5-4684-A139-8D2A30DD4475}.Debug|x86.ActiveCfg = Debug|Any CPU - {570165EC-62B5-4684-A139-8D2A30DD4475}.Debug|x86.Build.0 = Debug|Any CPU - {570165EC-62B5-4684-A139-8D2A30DD4475}.Release|Any CPU.ActiveCfg = Release|Any CPU - {570165EC-62B5-4684-A139-8D2A30DD4475}.Release|Any CPU.Build.0 = Release|Any CPU - {570165EC-62B5-4684-A139-8D2A30DD4475}.Release|x64.ActiveCfg = Release|Any CPU - {570165EC-62B5-4684-A139-8D2A30DD4475}.Release|x64.Build.0 = Release|Any CPU - {570165EC-62B5-4684-A139-8D2A30DD4475}.Release|x86.ActiveCfg = Release|Any CPU - {570165EC-62B5-4684-A139-8D2A30DD4475}.Release|x86.Build.0 = Release|Any CPU - {73DA578D-A63F-4956-83ED-6D7102E09140}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {73DA578D-A63F-4956-83ED-6D7102E09140}.Debug|Any CPU.Build.0 = Debug|Any CPU - {73DA578D-A63F-4956-83ED-6D7102E09140}.Debug|x64.ActiveCfg = Debug|Any CPU - {73DA578D-A63F-4956-83ED-6D7102E09140}.Debug|x64.Build.0 = Debug|Any CPU - {73DA578D-A63F-4956-83ED-6D7102E09140}.Debug|x86.ActiveCfg = Debug|Any CPU - {73DA578D-A63F-4956-83ED-6D7102E09140}.Debug|x86.Build.0 = Debug|Any CPU - {73DA578D-A63F-4956-83ED-6D7102E09140}.Release|Any CPU.ActiveCfg = Release|Any CPU - {73DA578D-A63F-4956-83ED-6D7102E09140}.Release|Any CPU.Build.0 = Release|Any CPU - {73DA578D-A63F-4956-83ED-6D7102E09140}.Release|x64.ActiveCfg = Release|Any CPU - {73DA578D-A63F-4956-83ED-6D7102E09140}.Release|x64.Build.0 = Release|Any CPU - {73DA578D-A63F-4956-83ED-6D7102E09140}.Release|x86.ActiveCfg = Release|Any CPU - {73DA578D-A63F-4956-83ED-6D7102E09140}.Release|x86.Build.0 = Release|Any CPU - {6D4BD85A-A262-44C6-8572-FE3A30410BF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6D4BD85A-A262-44C6-8572-FE3A30410BF3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6D4BD85A-A262-44C6-8572-FE3A30410BF3}.Debug|x64.ActiveCfg = Debug|Any CPU - {6D4BD85A-A262-44C6-8572-FE3A30410BF3}.Debug|x64.Build.0 = Debug|Any CPU - {6D4BD85A-A262-44C6-8572-FE3A30410BF3}.Debug|x86.ActiveCfg = Debug|Any CPU - {6D4BD85A-A262-44C6-8572-FE3A30410BF3}.Debug|x86.Build.0 = Debug|Any CPU - {6D4BD85A-A262-44C6-8572-FE3A30410BF3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6D4BD85A-A262-44C6-8572-FE3A30410BF3}.Release|Any CPU.Build.0 = Release|Any CPU - {6D4BD85A-A262-44C6-8572-FE3A30410BF3}.Release|x64.ActiveCfg = Release|Any CPU - {6D4BD85A-A262-44C6-8572-FE3A30410BF3}.Release|x64.Build.0 = Release|Any CPU - {6D4BD85A-A262-44C6-8572-FE3A30410BF3}.Release|x86.ActiveCfg = Release|Any CPU - {6D4BD85A-A262-44C6-8572-FE3A30410BF3}.Release|x86.Build.0 = Release|Any CPU - {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Debug|x64.ActiveCfg = Debug|Any CPU - {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Debug|x64.Build.0 = Debug|Any CPU - {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Debug|x86.ActiveCfg = Debug|Any CPU - {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Debug|x86.Build.0 = Debug|Any CPU - {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|Any CPU.Build.0 = Release|Any CPU - {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|x64.ActiveCfg = Release|Any CPU - {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|x64.Build.0 = Release|Any CPU - {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|x86.ActiveCfg = Release|Any CPU - {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|x86.Build.0 = Release|Any CPU - {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|x64.ActiveCfg = Debug|Any CPU - {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|x64.Build.0 = Debug|Any CPU - {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|x86.ActiveCfg = Debug|Any CPU - {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|x86.Build.0 = Debug|Any CPU - {1F604666-BB0F-413E-922D-9D37C6073285}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1F604666-BB0F-413E-922D-9D37C6073285}.Release|Any CPU.Build.0 = Release|Any CPU - {1F604666-BB0F-413E-922D-9D37C6073285}.Release|x64.ActiveCfg = Release|Any CPU - {1F604666-BB0F-413E-922D-9D37C6073285}.Release|x64.Build.0 = Release|Any CPU - {1F604666-BB0F-413E-922D-9D37C6073285}.Release|x86.ActiveCfg = Release|Any CPU - {1F604666-BB0F-413E-922D-9D37C6073285}.Release|x86.Build.0 = Release|Any CPU - {CF2C1EB6-8449-4B35-B8C7-F43D6D90632D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CF2C1EB6-8449-4B35-B8C7-F43D6D90632D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CF2C1EB6-8449-4B35-B8C7-F43D6D90632D}.Debug|x64.ActiveCfg = Debug|Any CPU - {CF2C1EB6-8449-4B35-B8C7-F43D6D90632D}.Debug|x64.Build.0 = Debug|Any CPU - {CF2C1EB6-8449-4B35-B8C7-F43D6D90632D}.Debug|x86.ActiveCfg = Debug|Any CPU - {CF2C1EB6-8449-4B35-B8C7-F43D6D90632D}.Debug|x86.Build.0 = Debug|Any CPU - {CF2C1EB6-8449-4B35-B8C7-F43D6D90632D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CF2C1EB6-8449-4B35-B8C7-F43D6D90632D}.Release|Any CPU.Build.0 = Release|Any CPU - {CF2C1EB6-8449-4B35-B8C7-F43D6D90632D}.Release|x64.ActiveCfg = Release|Any CPU - {CF2C1EB6-8449-4B35-B8C7-F43D6D90632D}.Release|x64.Build.0 = Release|Any CPU - {CF2C1EB6-8449-4B35-B8C7-F43D6D90632D}.Release|x86.ActiveCfg = Release|Any CPU - {CF2C1EB6-8449-4B35-B8C7-F43D6D90632D}.Release|x86.Build.0 = Release|Any CPU - {9CD2C116-D133-4FE4-97DA-A9FEAFF045F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9CD2C116-D133-4FE4-97DA-A9FEAFF045F1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9CD2C116-D133-4FE4-97DA-A9FEAFF045F1}.Debug|x64.ActiveCfg = Debug|Any CPU - {9CD2C116-D133-4FE4-97DA-A9FEAFF045F1}.Debug|x64.Build.0 = Debug|Any CPU - {9CD2C116-D133-4FE4-97DA-A9FEAFF045F1}.Debug|x86.ActiveCfg = Debug|Any CPU - {9CD2C116-D133-4FE4-97DA-A9FEAFF045F1}.Debug|x86.Build.0 = Debug|Any CPU - {9CD2C116-D133-4FE4-97DA-A9FEAFF045F1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9CD2C116-D133-4FE4-97DA-A9FEAFF045F1}.Release|Any CPU.Build.0 = Release|Any CPU - {9CD2C116-D133-4FE4-97DA-A9FEAFF045F1}.Release|x64.ActiveCfg = Release|Any CPU - {9CD2C116-D133-4FE4-97DA-A9FEAFF045F1}.Release|x64.Build.0 = Release|Any CPU - {9CD2C116-D133-4FE4-97DA-A9FEAFF045F1}.Release|x86.ActiveCfg = Release|Any CPU - {9CD2C116-D133-4FE4-97DA-A9FEAFF045F1}.Release|x86.Build.0 = Release|Any CPU - {F4097194-9415-418A-AB4E-315C5D5466AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F4097194-9415-418A-AB4E-315C5D5466AF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F4097194-9415-418A-AB4E-315C5D5466AF}.Debug|x64.ActiveCfg = Debug|Any CPU - {F4097194-9415-418A-AB4E-315C5D5466AF}.Debug|x64.Build.0 = Debug|Any CPU - {F4097194-9415-418A-AB4E-315C5D5466AF}.Debug|x86.ActiveCfg = Debug|Any CPU - {F4097194-9415-418A-AB4E-315C5D5466AF}.Debug|x86.Build.0 = Debug|Any CPU - {F4097194-9415-418A-AB4E-315C5D5466AF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F4097194-9415-418A-AB4E-315C5D5466AF}.Release|Any CPU.Build.0 = Release|Any CPU - {F4097194-9415-418A-AB4E-315C5D5466AF}.Release|x64.ActiveCfg = Release|Any CPU - {F4097194-9415-418A-AB4E-315C5D5466AF}.Release|x64.Build.0 = Release|Any CPU - {F4097194-9415-418A-AB4E-315C5D5466AF}.Release|x86.ActiveCfg = Release|Any CPU - {F4097194-9415-418A-AB4E-315C5D5466AF}.Release|x86.Build.0 = Release|Any CPU - {6DFA30D7-1679-4333-9779-6FB678E48EF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6DFA30D7-1679-4333-9779-6FB678E48EF5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6DFA30D7-1679-4333-9779-6FB678E48EF5}.Debug|x64.ActiveCfg = Debug|Any CPU - {6DFA30D7-1679-4333-9779-6FB678E48EF5}.Debug|x64.Build.0 = Debug|Any CPU - {6DFA30D7-1679-4333-9779-6FB678E48EF5}.Debug|x86.ActiveCfg = Debug|Any CPU - {6DFA30D7-1679-4333-9779-6FB678E48EF5}.Debug|x86.Build.0 = Debug|Any CPU - {6DFA30D7-1679-4333-9779-6FB678E48EF5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6DFA30D7-1679-4333-9779-6FB678E48EF5}.Release|Any CPU.Build.0 = Release|Any CPU - {6DFA30D7-1679-4333-9779-6FB678E48EF5}.Release|x64.ActiveCfg = Release|Any CPU - {6DFA30D7-1679-4333-9779-6FB678E48EF5}.Release|x64.Build.0 = Release|Any CPU - {6DFA30D7-1679-4333-9779-6FB678E48EF5}.Release|x86.ActiveCfg = Release|Any CPU - {6DFA30D7-1679-4333-9779-6FB678E48EF5}.Release|x86.Build.0 = Release|Any CPU - {DF9BFD82-D937-4907-B0B4-64670417115F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DF9BFD82-D937-4907-B0B4-64670417115F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DF9BFD82-D937-4907-B0B4-64670417115F}.Debug|x64.ActiveCfg = Debug|Any CPU - {DF9BFD82-D937-4907-B0B4-64670417115F}.Debug|x64.Build.0 = Debug|Any CPU - {DF9BFD82-D937-4907-B0B4-64670417115F}.Debug|x86.ActiveCfg = Debug|Any CPU - {DF9BFD82-D937-4907-B0B4-64670417115F}.Debug|x86.Build.0 = Debug|Any CPU - {DF9BFD82-D937-4907-B0B4-64670417115F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DF9BFD82-D937-4907-B0B4-64670417115F}.Release|Any CPU.Build.0 = Release|Any CPU - {DF9BFD82-D937-4907-B0B4-64670417115F}.Release|x64.ActiveCfg = Release|Any CPU - {DF9BFD82-D937-4907-B0B4-64670417115F}.Release|x64.Build.0 = Release|Any CPU - {DF9BFD82-D937-4907-B0B4-64670417115F}.Release|x86.ActiveCfg = Release|Any CPU - {DF9BFD82-D937-4907-B0B4-64670417115F}.Release|x86.Build.0 = Release|Any CPU - {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Debug|x64.ActiveCfg = Debug|Any CPU - {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Debug|x64.Build.0 = Debug|Any CPU - {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Debug|x86.ActiveCfg = Debug|Any CPU - {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Debug|x86.Build.0 = Debug|Any CPU - {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|Any CPU.Build.0 = Release|Any CPU - {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|x64.ActiveCfg = Release|Any CPU - {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|x64.Build.0 = Release|Any CPU - {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|x86.ActiveCfg = Release|Any CPU - {09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {C0EC9E70-EB2E-436F-9D94-FA16FA774123} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF} - {97EE048B-16C0-43F6-BDA9-4E762B2F579F} = {026FBC6C-AF76-4568-9B87-EC73457899FD} - {0B959765-40D2-43B5-87EE-FE2FEF9DBED5} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} - {570165EC-62B5-4684-A139-8D2A30DD4475} = {026FBC6C-AF76-4568-9B87-EC73457899FD} - {73DA578D-A63F-4956-83ED-6D7102E09140} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} - {6D4BD85A-A262-44C6-8572-FE3A30410BF3} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} - {026FBC6C-AF76-4568-9B87-EC73457899FD} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF} - {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D} = {026FBC6C-AF76-4568-9B87-EC73457899FD} - {1F604666-BB0F-413E-922D-9D37C6073285} = {076E1AE4-FD25-4684-B826-CAAE37FEA0AA} - {CF2C1EB6-8449-4B35-B8C7-F43D6D90632D} = {026FBC6C-AF76-4568-9B87-EC73457899FD} - {9CD2C116-D133-4FE4-97DA-A9FEAFF045F1} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} - {F4097194-9415-418A-AB4E-315C5D5466AF} = {026FBC6C-AF76-4568-9B87-EC73457899FD} - {6DFA30D7-1679-4333-9779-6FB678E48EF5} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} - {09C0C8D8-B721-4955-8889-55CB149C3B5C} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {A2421882-8F0A-4905-928F-B550B192F9A4} - EndGlobalSection -EndGlobal +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28606.126 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C5B4D998-CECB-454D-9F32-085A897577BE}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + .travis.yml = .travis.yml + appveyor.yml = appveyor.yml + Build.ps1 = Build.ps1 + build.sh = build.sh + Directory.Build.props = Directory.Build.props + README.md = README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{026FBC6C-AF76-4568-9B87-EC73457899FD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{076E1AE4-FD25-4684-B826-CAAE37FEA0AA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCoreExampleTests", "test\JsonApiDotNetCoreExampleTests\JsonApiDotNetCoreExampleTests.csproj", "{CAF331F8-9255-4D72-A1A8-A54141E99F1E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NoEntityFrameworkTests", "test\NoEntityFrameworkTests\NoEntityFrameworkTests.csproj", "{4F15A8F8-5BC6-45A1-BC51-03F921B726A4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "test\UnitTests\UnitTests.csproj", "{8788FF65-C2B6-40B2-A3A0-1E3D91C02664}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscoveryTests", "test\DiscoveryTests\DiscoveryTests.csproj", "{03032A2F-664D-4DD8-A82F-AD8A482EDD85}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "benchmarks\Benchmarks.csproj", "{DF0FCFB2-CB12-44BA-BBB5-1BE0BCFCD14C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCoreExample", "src\Examples\JsonApiDotNetCoreExample\JsonApiDotNetCoreExample.csproj", "{C916EBDA-3429-4FEA-AFB3-DF7CA32A8C6A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NoEntityFrameworkExample", "src\Examples\NoEntityFrameworkExample\NoEntityFrameworkExample.csproj", "{789085E1-048F-4996-B600-791B9CA3A663}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReportsExample", "src\Examples\ReportsExample\ReportsExample.csproj", "{8BCFF95F-4850-427C-AEDB-B5B4F62B2C7B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCore", "src\JsonApiDotNetCore\JsonApiDotNetCore.csproj", "{21D27239-138D-4604-8E49-DCBE41BCE4C8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GettingStarted", "src\Examples\GettingStarted\GettingStarted.csproj", "{067FFD7A-C66B-473D-8471-37F5C95DF61C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationTests", "test\IntegrationTests\IntegrationTests.csproj", "{CEB08B86-6BF1-4227-B20F-45AE9C1CC6D9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CAF331F8-9255-4D72-A1A8-A54141E99F1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CAF331F8-9255-4D72-A1A8-A54141E99F1E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CAF331F8-9255-4D72-A1A8-A54141E99F1E}.Debug|x64.ActiveCfg = Debug|Any CPU + {CAF331F8-9255-4D72-A1A8-A54141E99F1E}.Debug|x86.ActiveCfg = Debug|Any CPU + {CAF331F8-9255-4D72-A1A8-A54141E99F1E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CAF331F8-9255-4D72-A1A8-A54141E99F1E}.Release|Any CPU.Build.0 = Release|Any CPU + {CAF331F8-9255-4D72-A1A8-A54141E99F1E}.Release|x64.ActiveCfg = Release|Any CPU + {CAF331F8-9255-4D72-A1A8-A54141E99F1E}.Release|x86.ActiveCfg = Release|Any CPU + {4F15A8F8-5BC6-45A1-BC51-03F921B726A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F15A8F8-5BC6-45A1-BC51-03F921B726A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F15A8F8-5BC6-45A1-BC51-03F921B726A4}.Debug|x64.ActiveCfg = Debug|Any CPU + {4F15A8F8-5BC6-45A1-BC51-03F921B726A4}.Debug|x64.Build.0 = Debug|Any CPU + {4F15A8F8-5BC6-45A1-BC51-03F921B726A4}.Debug|x86.ActiveCfg = Debug|Any CPU + {4F15A8F8-5BC6-45A1-BC51-03F921B726A4}.Debug|x86.Build.0 = Debug|Any CPU + {4F15A8F8-5BC6-45A1-BC51-03F921B726A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F15A8F8-5BC6-45A1-BC51-03F921B726A4}.Release|Any CPU.Build.0 = Release|Any CPU + {4F15A8F8-5BC6-45A1-BC51-03F921B726A4}.Release|x64.ActiveCfg = Release|Any CPU + {4F15A8F8-5BC6-45A1-BC51-03F921B726A4}.Release|x64.Build.0 = Release|Any CPU + {4F15A8F8-5BC6-45A1-BC51-03F921B726A4}.Release|x86.ActiveCfg = Release|Any CPU + {4F15A8F8-5BC6-45A1-BC51-03F921B726A4}.Release|x86.Build.0 = Release|Any CPU + {8788FF65-C2B6-40B2-A3A0-1E3D91C02664}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8788FF65-C2B6-40B2-A3A0-1E3D91C02664}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8788FF65-C2B6-40B2-A3A0-1E3D91C02664}.Debug|x64.ActiveCfg = Debug|Any CPU + {8788FF65-C2B6-40B2-A3A0-1E3D91C02664}.Debug|x64.Build.0 = Debug|Any CPU + {8788FF65-C2B6-40B2-A3A0-1E3D91C02664}.Debug|x86.ActiveCfg = Debug|Any CPU + {8788FF65-C2B6-40B2-A3A0-1E3D91C02664}.Debug|x86.Build.0 = Debug|Any CPU + {8788FF65-C2B6-40B2-A3A0-1E3D91C02664}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8788FF65-C2B6-40B2-A3A0-1E3D91C02664}.Release|Any CPU.Build.0 = Release|Any CPU + {8788FF65-C2B6-40B2-A3A0-1E3D91C02664}.Release|x64.ActiveCfg = Release|Any CPU + {8788FF65-C2B6-40B2-A3A0-1E3D91C02664}.Release|x64.Build.0 = Release|Any CPU + {8788FF65-C2B6-40B2-A3A0-1E3D91C02664}.Release|x86.ActiveCfg = Release|Any CPU + {8788FF65-C2B6-40B2-A3A0-1E3D91C02664}.Release|x86.Build.0 = Release|Any CPU + {03032A2F-664D-4DD8-A82F-AD8A482EDD85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03032A2F-664D-4DD8-A82F-AD8A482EDD85}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03032A2F-664D-4DD8-A82F-AD8A482EDD85}.Debug|x64.ActiveCfg = Debug|Any CPU + {03032A2F-664D-4DD8-A82F-AD8A482EDD85}.Debug|x64.Build.0 = Debug|Any CPU + {03032A2F-664D-4DD8-A82F-AD8A482EDD85}.Debug|x86.ActiveCfg = Debug|Any CPU + {03032A2F-664D-4DD8-A82F-AD8A482EDD85}.Debug|x86.Build.0 = Debug|Any CPU + {03032A2F-664D-4DD8-A82F-AD8A482EDD85}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03032A2F-664D-4DD8-A82F-AD8A482EDD85}.Release|Any CPU.Build.0 = Release|Any CPU + {03032A2F-664D-4DD8-A82F-AD8A482EDD85}.Release|x64.ActiveCfg = Release|Any CPU + {03032A2F-664D-4DD8-A82F-AD8A482EDD85}.Release|x64.Build.0 = Release|Any CPU + {03032A2F-664D-4DD8-A82F-AD8A482EDD85}.Release|x86.ActiveCfg = Release|Any CPU + {03032A2F-664D-4DD8-A82F-AD8A482EDD85}.Release|x86.Build.0 = Release|Any CPU + {DF0FCFB2-CB12-44BA-BBB5-1BE0BCFCD14C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF0FCFB2-CB12-44BA-BBB5-1BE0BCFCD14C}.Debug|x64.ActiveCfg = Debug|Any CPU + {DF0FCFB2-CB12-44BA-BBB5-1BE0BCFCD14C}.Debug|x64.Build.0 = Debug|Any CPU + {DF0FCFB2-CB12-44BA-BBB5-1BE0BCFCD14C}.Debug|x86.ActiveCfg = Debug|Any CPU + {DF0FCFB2-CB12-44BA-BBB5-1BE0BCFCD14C}.Debug|x86.Build.0 = Debug|Any CPU + {DF0FCFB2-CB12-44BA-BBB5-1BE0BCFCD14C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF0FCFB2-CB12-44BA-BBB5-1BE0BCFCD14C}.Release|Any CPU.Build.0 = Release|Any CPU + {DF0FCFB2-CB12-44BA-BBB5-1BE0BCFCD14C}.Release|x64.ActiveCfg = Release|Any CPU + {DF0FCFB2-CB12-44BA-BBB5-1BE0BCFCD14C}.Release|x64.Build.0 = Release|Any CPU + {DF0FCFB2-CB12-44BA-BBB5-1BE0BCFCD14C}.Release|x86.ActiveCfg = Release|Any CPU + {DF0FCFB2-CB12-44BA-BBB5-1BE0BCFCD14C}.Release|x86.Build.0 = Release|Any CPU + {C916EBDA-3429-4FEA-AFB3-DF7CA32A8C6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C916EBDA-3429-4FEA-AFB3-DF7CA32A8C6A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C916EBDA-3429-4FEA-AFB3-DF7CA32A8C6A}.Debug|x64.ActiveCfg = Debug|Any CPU + {C916EBDA-3429-4FEA-AFB3-DF7CA32A8C6A}.Debug|x86.ActiveCfg = Debug|Any CPU + {C916EBDA-3429-4FEA-AFB3-DF7CA32A8C6A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C916EBDA-3429-4FEA-AFB3-DF7CA32A8C6A}.Release|Any CPU.Build.0 = Release|Any CPU + {C916EBDA-3429-4FEA-AFB3-DF7CA32A8C6A}.Release|x64.ActiveCfg = Release|Any CPU + {C916EBDA-3429-4FEA-AFB3-DF7CA32A8C6A}.Release|x86.ActiveCfg = Release|Any CPU + {789085E1-048F-4996-B600-791B9CA3A663}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {789085E1-048F-4996-B600-791B9CA3A663}.Debug|Any CPU.Build.0 = Debug|Any CPU + {789085E1-048F-4996-B600-791B9CA3A663}.Debug|x64.ActiveCfg = Debug|Any CPU + {789085E1-048F-4996-B600-791B9CA3A663}.Debug|x64.Build.0 = Debug|Any CPU + {789085E1-048F-4996-B600-791B9CA3A663}.Debug|x86.ActiveCfg = Debug|Any CPU + {789085E1-048F-4996-B600-791B9CA3A663}.Debug|x86.Build.0 = Debug|Any CPU + {789085E1-048F-4996-B600-791B9CA3A663}.Release|Any CPU.ActiveCfg = Release|Any CPU + {789085E1-048F-4996-B600-791B9CA3A663}.Release|Any CPU.Build.0 = Release|Any CPU + {789085E1-048F-4996-B600-791B9CA3A663}.Release|x64.ActiveCfg = Release|Any CPU + {789085E1-048F-4996-B600-791B9CA3A663}.Release|x64.Build.0 = Release|Any CPU + {789085E1-048F-4996-B600-791B9CA3A663}.Release|x86.ActiveCfg = Release|Any CPU + {789085E1-048F-4996-B600-791B9CA3A663}.Release|x86.Build.0 = Release|Any CPU + {8BCFF95F-4850-427C-AEDB-B5B4F62B2C7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8BCFF95F-4850-427C-AEDB-B5B4F62B2C7B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8BCFF95F-4850-427C-AEDB-B5B4F62B2C7B}.Debug|x64.ActiveCfg = Debug|Any CPU + {8BCFF95F-4850-427C-AEDB-B5B4F62B2C7B}.Debug|x64.Build.0 = Debug|Any CPU + {8BCFF95F-4850-427C-AEDB-B5B4F62B2C7B}.Debug|x86.ActiveCfg = Debug|Any CPU + {8BCFF95F-4850-427C-AEDB-B5B4F62B2C7B}.Debug|x86.Build.0 = Debug|Any CPU + {8BCFF95F-4850-427C-AEDB-B5B4F62B2C7B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8BCFF95F-4850-427C-AEDB-B5B4F62B2C7B}.Release|Any CPU.Build.0 = Release|Any CPU + {8BCFF95F-4850-427C-AEDB-B5B4F62B2C7B}.Release|x64.ActiveCfg = Release|Any CPU + {8BCFF95F-4850-427C-AEDB-B5B4F62B2C7B}.Release|x64.Build.0 = Release|Any CPU + {8BCFF95F-4850-427C-AEDB-B5B4F62B2C7B}.Release|x86.ActiveCfg = Release|Any CPU + {8BCFF95F-4850-427C-AEDB-B5B4F62B2C7B}.Release|x86.Build.0 = Release|Any CPU + {21D27239-138D-4604-8E49-DCBE41BCE4C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21D27239-138D-4604-8E49-DCBE41BCE4C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21D27239-138D-4604-8E49-DCBE41BCE4C8}.Debug|x64.ActiveCfg = Debug|Any CPU + {21D27239-138D-4604-8E49-DCBE41BCE4C8}.Debug|x86.ActiveCfg = Debug|Any CPU + {21D27239-138D-4604-8E49-DCBE41BCE4C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21D27239-138D-4604-8E49-DCBE41BCE4C8}.Release|Any CPU.Build.0 = Release|Any CPU + {21D27239-138D-4604-8E49-DCBE41BCE4C8}.Release|x64.ActiveCfg = Release|Any CPU + {21D27239-138D-4604-8E49-DCBE41BCE4C8}.Release|x86.ActiveCfg = Release|Any CPU + {067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x64.ActiveCfg = Debug|Any CPU + {067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x64.Build.0 = Debug|Any CPU + {067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x86.ActiveCfg = Debug|Any CPU + {067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x86.Build.0 = Debug|Any CPU + {067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|Any CPU.Build.0 = Release|Any CPU + {067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x64.ActiveCfg = Release|Any CPU + {067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x64.Build.0 = Release|Any CPU + {067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x86.ActiveCfg = Release|Any CPU + {067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x86.Build.0 = Release|Any CPU + {CEB08B86-6BF1-4227-B20F-45AE9C1CC6D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CEB08B86-6BF1-4227-B20F-45AE9C1CC6D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CEB08B86-6BF1-4227-B20F-45AE9C1CC6D9}.Debug|x64.ActiveCfg = Debug|Any CPU + {CEB08B86-6BF1-4227-B20F-45AE9C1CC6D9}.Debug|x64.Build.0 = Debug|Any CPU + {CEB08B86-6BF1-4227-B20F-45AE9C1CC6D9}.Debug|x86.ActiveCfg = Debug|Any CPU + {CEB08B86-6BF1-4227-B20F-45AE9C1CC6D9}.Debug|x86.Build.0 = Debug|Any CPU + {CEB08B86-6BF1-4227-B20F-45AE9C1CC6D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CEB08B86-6BF1-4227-B20F-45AE9C1CC6D9}.Release|Any CPU.Build.0 = Release|Any CPU + {CEB08B86-6BF1-4227-B20F-45AE9C1CC6D9}.Release|x64.ActiveCfg = Release|Any CPU + {CEB08B86-6BF1-4227-B20F-45AE9C1CC6D9}.Release|x64.Build.0 = Release|Any CPU + {CEB08B86-6BF1-4227-B20F-45AE9C1CC6D9}.Release|x86.ActiveCfg = Release|Any CPU + {CEB08B86-6BF1-4227-B20F-45AE9C1CC6D9}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {026FBC6C-AF76-4568-9B87-EC73457899FD} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF} + {CAF331F8-9255-4D72-A1A8-A54141E99F1E} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} + {4F15A8F8-5BC6-45A1-BC51-03F921B726A4} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} + {8788FF65-C2B6-40B2-A3A0-1E3D91C02664} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} + {03032A2F-664D-4DD8-A82F-AD8A482EDD85} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} + {DF0FCFB2-CB12-44BA-BBB5-1BE0BCFCD14C} = {076E1AE4-FD25-4684-B826-CAAE37FEA0AA} + {C916EBDA-3429-4FEA-AFB3-DF7CA32A8C6A} = {026FBC6C-AF76-4568-9B87-EC73457899FD} + {789085E1-048F-4996-B600-791B9CA3A663} = {026FBC6C-AF76-4568-9B87-EC73457899FD} + {8BCFF95F-4850-427C-AEDB-B5B4F62B2C7B} = {026FBC6C-AF76-4568-9B87-EC73457899FD} + {21D27239-138D-4604-8E49-DCBE41BCE4C8} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF} + {CEB08B86-6BF1-4227-B20F-45AE9C1CC6D9} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A2421882-8F0A-4905-928F-B550B192F9A4} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index d0a5bb2cf2..3c91c6492e 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ See [the documentation](https://json-api-dotnet.github.io/#/) for detailed usage ```csharp public class Article : Identifiable -{ +{ [Attr("name")] public string Name { get; set; } } @@ -53,10 +53,12 @@ public class Article : Identifiable ```csharp public class ArticlesController : JsonApiController
{ - public ArticlesController( - IJsonApiContext jsonApiContext, - IResourceService
resourceService) - : base(jsonApiContext, resourceService) { } + public ArticlesController( + IJsonApiOptions jsonApiOptions, + IResourceService
resourceService, + ILoggerFactory loggerFactory) + : base(jsonApiOptions, resourceService, loggerFactory) + { } } ``` @@ -79,7 +81,7 @@ public class Startup ### Development -Restore all nuget packages with: +Restore all NuGet packages with: ```bash dotnet restore @@ -87,11 +89,10 @@ dotnet restore #### Testing -Running tests locally requires access to a postgresql database. -If you have docker installed, this can be propped up via: +Running tests locally requires access to a PostgreSQL database. If you have docker installed, this can be propped up via: ```bash -docker run --rm --name jsonapi-dotnet-core-testing -e POSTGRES_DB=JsonApiDotNetCoreExample -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres +docker run --rm --name jsonapi-dotnet-core-testing -e POSTGRES_DB=JsonApiDotNetCoreExample -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres:12.0 ``` And then to run the tests: @@ -107,3 +108,14 @@ Sometimes the compiled files can be dirty / corrupt from other branches / failed ```bash dotnet clean ``` + +## Compatibility + +A lot of changes were introduced in v4.0.0, the following chart should help you with compatibility issues between .NET Core versions + +| .NET Core Version | JADNC Version | +| ----------------- | ------------- | +| 2.* | v3.* | +| 3.* | v4.* | + + diff --git a/appveyor.yml b/appveyor.yml index 6d62e7f0e2..406d1e974e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,5 @@ version: '{build}' -os: Visual Studio 2017 +os: Visual Studio 2019 environment: POSTGRES_PORT: tcp://localhost:5432 diff --git a/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs b/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs index 05728321c3..1432afecd8 100644 --- a/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs +++ b/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs @@ -1,10 +1,11 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes.Exporters; using BenchmarkDotNet.Attributes.Jobs; +using System; namespace Benchmarks.LinkBuilder { - [MarkdownExporter, SimpleJob(launchCount : 3, warmupCount : 10, targetCount : 20), MemoryDiagnoser] + [MarkdownExporter, SimpleJob(launchCount: 3, warmupCount: 10, targetCount: 20), MemoryDiagnoser] public class LinkBuilder_GetNamespaceFromPath_Benchmarks { private const string PATH = "/api/some-really-long-namespace-path/resources/current/articles"; @@ -14,7 +15,7 @@ public class LinkBuilder_GetNamespaceFromPath_Benchmarks public void UsingSplit() => GetNamespaceFromPath_BySplitting(PATH, ENTITY_NAME); [Benchmark] - public void Current() => GetNameSpaceFromPath_Current(PATH, ENTITY_NAME); + public void Current() => GetNameSpaceFromPathCurrent(PATH, ENTITY_NAME); public static string GetNamespaceFromPath_BySplitting(string path, string entityName) { @@ -32,7 +33,38 @@ public static string GetNamespaceFromPath_BySplitting(string path, string entity return nSpace; } - public static string GetNameSpaceFromPath_Current(string path, string entityName) - => JsonApiDotNetCore.Builders.LinkBuilder.GetNamespaceFromPath(path, entityName); + public static string GetNameSpaceFromPathCurrent(string path, string entityName) + { + + var entityNameSpan = entityName.AsSpan(); + var pathSpan = path.AsSpan(); + const char delimiter = '/'; + for (var i = 0; i < pathSpan.Length; i++) + { + if (pathSpan[i].Equals(delimiter)) + { + var nextPosition = i + 1; + if (pathSpan.Length > i + entityNameSpan.Length) + { + var possiblePathSegment = pathSpan.Slice(nextPosition, entityNameSpan.Length); + if (entityNameSpan.SequenceEqual(possiblePathSegment)) + { + // check to see if it's the last position in the string + // or if the next character is a / + var lastCharacterPosition = nextPosition + entityNameSpan.Length; + + if (lastCharacterPosition == pathSpan.Length || pathSpan.Length >= lastCharacterPosition + 2 && pathSpan[lastCharacterPosition].Equals(delimiter)) + { + return pathSpan.Slice(0, i).ToString(); + } + } + } + } + } + + return string.Empty; + + + } } } diff --git a/benchmarks/Program.cs b/benchmarks/Program.cs index 9a2c45dffb..bd504c670f 100644 --- a/benchmarks/Program.cs +++ b/benchmarks/Program.cs @@ -1,4 +1,4 @@ -using BenchmarkDotNet.Running; +using BenchmarkDotNet.Running; using Benchmarks.JsonApiContext; using Benchmarks.LinkBuilder; using Benchmarks.Query; @@ -9,8 +9,8 @@ namespace Benchmarks { class Program { static void Main(string[] args) { var switcher = new BenchmarkSwitcher(new[] { - typeof(JsonApiDeserializer_Benchmarks), - typeof(JsonApiSerializer_Benchmarks), + typeof(JsonApideserializer_Benchmarks), + //typeof(JsonApiSerializer_Benchmarks), typeof(QueryParser_Benchmarks), typeof(LinkBuilder_GetNamespaceFromPath_Benchmarks), typeof(ContainsMediaTypeParameters_Benchmarks), diff --git a/benchmarks/Query/QueryParser_Benchmarks.cs b/benchmarks/Query/QueryParser_Benchmarks.cs index de82baa60f..68c08e8e79 100644 --- a/benchmarks/Query/QueryParser_Benchmarks.cs +++ b/benchmarks/Query/QueryParser_Benchmarks.cs @@ -5,6 +5,7 @@ using BenchmarkDotNet.Attributes.Jobs; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http.Internal; @@ -21,14 +22,14 @@ public class QueryParser_Benchmarks { private const string DESCENDING_SORT = "-" + ATTRIBUTE; public QueryParser_Benchmarks() { - var controllerContextMock = new Mock(); - controllerContextMock.Setup(m => m.RequestEntity).Returns(new ContextEntity { + var requestMock = new Mock(); + requestMock.Setup(m => m.GetRequestResource()).Returns(new ResourceContext { Attributes = new List { new AttrAttribute(ATTRIBUTE, ATTRIBUTE) } }); var options = new JsonApiOptions(); - _queryParser = new BenchmarkFacade(controllerContextMock.Object, options); + _queryParser = new BenchmarkFacade(requestMock.Object, options); } [Benchmark] @@ -56,10 +57,10 @@ private void Run(int iterations, Action action) { } // this facade allows us to expose and micro-benchmark protected methods - private class BenchmarkFacade : QueryParser { + private class BenchmarkFacade : QueryParameterDiscovery { public BenchmarkFacade( - IControllerContext controllerContext, - JsonApiOptions options) : base(controllerContext, options) { } + IRequestContext currentRequest, + JsonApiOptions options) : base(currentRequest, options) { } public void _ParseSortParameters(string value) => base.ParseSortParameters(value); } diff --git a/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs b/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs index ed64c98335..2e0a5c0232 100644 --- a/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs +++ b/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs @@ -14,7 +14,7 @@ public class ContainsMediaTypeParameters_Benchmarks [Benchmark] public void Current() - => JsonApiDotNetCore.Middleware.RequestMiddleware.ContainsMediaTypeParameters(MEDIA_TYPE); + => JsonApiDotNetCore.Middleware.CurrentRequestMiddleware.ContainsMediaTypeParameters(MEDIA_TYPE); private bool UsingSplitImpl(string mediaType) { diff --git a/benchmarks/Serialization/JsonApiDeserializer_Benchmarks.cs b/benchmarks/Serialization/JsonApiDeserializer_Benchmarks.cs index 983bc07f90..a2487052d8 100644 --- a/benchmarks/Serialization/JsonApiDeserializer_Benchmarks.cs +++ b/benchmarks/Serialization/JsonApiDeserializer_Benchmarks.cs @@ -4,17 +4,19 @@ using BenchmarkDotNet.Attributes.Exporters; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal.Generics; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + using JsonApiDotNetCore.Services; using Moq; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; - -namespace Benchmarks.Serialization { +namespace Benchmarks.Serialization +{ [MarkdownExporter] - public class JsonApiDeserializer_Benchmarks { + public class JsonApideserializer_Benchmarks { private const string TYPE_NAME = "simple-types"; private static readonly string Content = JsonConvert.SerializeObject(new Document { Data = new ResourceObject { @@ -29,28 +31,31 @@ public class JsonApiDeserializer_Benchmarks { } }); - private readonly JsonApiDeSerializer _jsonApiDeSerializer; + private readonly JsonApideserializer _jsonApideserializer; - public JsonApiDeserializer_Benchmarks() { + public JsonApideserializer_Benchmarks() { var resourceGraphBuilder = new ResourceGraphBuilder(); resourceGraphBuilder.AddResource(TYPE_NAME); var resourceGraph = resourceGraphBuilder.Build(); + var currentRequestMock = new Mock(); + + currentRequestMock.Setup(m => m.GetUpdatedAttributes()).Returns(new Dictionary()); var jsonApiContextMock = new Mock(); jsonApiContextMock.SetupAllProperties(); jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); + jsonApiContextMock.Setup(m => m.RequestManager.GetUpdatedAttributes()).Returns(new Dictionary()); var jsonApiOptions = new JsonApiOptions(); jsonApiOptions.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - _jsonApiDeSerializer = new JsonApiDeSerializer(jsonApiContextMock.Object); + _jsonApideserializer = new JsonApideserializer(jsonApiContextMock.Object, currentRequestMock.Object); } [Benchmark] - public object DeserializeSimpleObject() => _jsonApiDeSerializer.Deserialize(Content); + public object DeserializeSimpleObject() => _jsonApideserializer.Deserialize(Content); private class SimpleType : Identifiable { [Attr("name")] diff --git a/benchmarks/Serialization/JsonApiSerializer_Benchmarks.cs b/benchmarks/Serialization/JsonApiSerializer_Benchmarks.cs index d80540434b..19e585bdc3 100644 --- a/benchmarks/Serialization/JsonApiSerializer_Benchmarks.cs +++ b/benchmarks/Serialization/JsonApiSerializer_Benchmarks.cs @@ -1,49 +1,51 @@ -using System.Collections.Generic; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Attributes.Exporters; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal.Generics; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Services; -using Moq; -using Newtonsoft.Json.Serialization; - -namespace Benchmarks.Serialization { - [MarkdownExporter] - public class JsonApiSerializer_Benchmarks { - private const string TYPE_NAME = "simple-types"; - private static readonly SimpleType Content = new SimpleType(); - - private readonly JsonApiSerializer _jsonApiSerializer; - - public JsonApiSerializer_Benchmarks() { - var resourceGraphBuilder = new ResourceGraphBuilder(); - resourceGraphBuilder.AddResource(TYPE_NAME); - var resourceGraph = resourceGraphBuilder.Build(); - - var jsonApiContextMock = new Mock(); - jsonApiContextMock.SetupAllProperties(); - jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); - - var jsonApiOptions = new JsonApiOptions(); - jsonApiOptions.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); - jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - - var genericProcessorFactoryMock = new Mock(); - - var documentBuilder = new DocumentBuilder(jsonApiContextMock.Object); - _jsonApiSerializer = new JsonApiSerializer(jsonApiContextMock.Object, documentBuilder); - } - - [Benchmark] - public object SerializeSimpleObject() => _jsonApiSerializer.Serialize(Content); - - private class SimpleType : Identifiable { - [Attr("name")] - public string Name { get; set; } - } - } -} +//using System.Collections.Generic; +//using BenchmarkDotNet.Attributes; +//using BenchmarkDotNet.Attributes.Exporters; +//using JsonApiDotNetCore.Builders; +//using JsonApiDotNetCore.Configuration; +//using JsonApiDotNetCore.Internal.Generics; +//using JsonApiDotNetCore.Models; +//using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + +//using JsonApiDotNetCore.Services; +//using Moq; +//using Newtonsoft.Json.Serialization; + +//namespace Benchmarks.Serialization { +// [MarkdownExporter] +// public class JsonApiSerializer_Benchmarks { +// private const string TYPE_NAME = "simple-types"; +// private static readonly SimpleType Content = new SimpleType(); + +// private readonly JsonApiSerializer _jsonApiSerializer; + +// public JsonApiSerializer_Benchmarks() { +// var resourceGraphBuilder = new ResourceGraphBuilder(); +// resourceGraphBuilder.AddResource(TYPE_NAME); +// var resourceGraph = resourceGraphBuilder.Build(); + +// var jsonApiContextMock = new Mock(); +// jsonApiContextMock.SetupAllProperties(); +// jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); +// jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); + +// var jsonApiOptions = new JsonApiOptions(); +// jsonApiOptions.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); +// jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); + +// var genericServiceFactoryMock = new Mock(); + +// var documentBuilder = new BaseDocumentBuilder(jsonApiContextMock.Object); +// _jsonApiSerializer = new JsonApiSerializer(jsonApiContextMock.Object, documentBuilder); +// } + +// [Benchmark] +// public object SerializeSimpleObject() => _jsonApiSerializer.Serialize(Content); + +// private class SimpleType : Identifiable { +// [Attr("name")] +// public string Name { get; set; } +// } +// } +//} diff --git a/build.sh b/build.sh index 1230bd6414..71989c80a7 100755 --- a/build.sh +++ b/build.sh @@ -8,5 +8,3 @@ dotnet restore dotnet test ./test/UnitTests/UnitTests.csproj dotnet test ./test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj dotnet test ./test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj -dotnet test ./test/OperationsExampleTests/OperationsExampleTests.csproj -dotnet test ./test/ResourceEntitySeparationExampleTests/ResourceEntitySeparationExampleTests.csproj diff --git a/markdownlint.config b/markdownlint.config new file mode 100644 index 0000000000..8f376b5c2a --- /dev/null +++ b/markdownlint.config @@ -0,0 +1,5 @@ +{ + "MD033": { + "allowed_elements": [ "p", "img", "p" ] + } +} \ No newline at end of file diff --git a/src/Examples/GettingStarted/Controllers/ArticlesController.cs b/src/Examples/GettingStarted/Controllers/ArticlesController.cs index 53517540b1..2bc928a46f 100644 --- a/src/Examples/GettingStarted/Controllers/ArticlesController.cs +++ b/src/Examples/GettingStarted/Controllers/ArticlesController.cs @@ -1,4 +1,5 @@ using GettingStarted.Models; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; @@ -7,9 +8,9 @@ namespace GettingStarted public class ArticlesController : JsonApiController
{ public ArticlesController( - IJsonApiContext jsonApiContext, - IResourceService
resourceService) - : base(jsonApiContext, resourceService) + IJsonApiOptions jsonApiOptions, + IResourceService
resourceService) + : base(jsonApiOptions, resourceService) { } } -} \ No newline at end of file +} diff --git a/src/Examples/GettingStarted/Controllers/PeopleController.cs b/src/Examples/GettingStarted/Controllers/PeopleController.cs index f3c0c4b868..95eac64346 100644 --- a/src/Examples/GettingStarted/Controllers/PeopleController.cs +++ b/src/Examples/GettingStarted/Controllers/PeopleController.cs @@ -1,4 +1,5 @@ using GettingStarted.Models; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; @@ -7,9 +8,9 @@ namespace GettingStarted public class PeopleController : JsonApiController { public PeopleController( - IJsonApiContext jsonApiContext, - IResourceService resourceService) - : base(jsonApiContext, resourceService) + IJsonApiOptions jsonApiOptions, + IResourceService resourceService) + : base(jsonApiOptions, resourceService) { } } -} \ No newline at end of file +} diff --git a/src/Examples/GettingStarted/Data/SampleDbContext.cs b/src/Examples/GettingStarted/Data/SampleDbContext.cs index 2f8fefb405..ede5e02baf 100644 --- a/src/Examples/GettingStarted/Data/SampleDbContext.cs +++ b/src/Examples/GettingStarted/Data/SampleDbContext.cs @@ -6,10 +6,7 @@ namespace GettingStarted { public class SampleDbContext : DbContext { - public SampleDbContext(DbContextOptions options) - : base(options) - { } - + public SampleDbContext(DbContextOptions options) : base(options) { } public DbSet
Articles { get; set; } public DbSet People { get; set; } public DbSet Models { get; set; } diff --git a/src/Examples/GettingStarted/GettingStarted.csproj b/src/Examples/GettingStarted/GettingStarted.csproj index ece976d5e5..048de21397 100644 --- a/src/Examples/GettingStarted/GettingStarted.csproj +++ b/src/Examples/GettingStarted/GettingStarted.csproj @@ -1,7 +1,6 @@ - - netcoreapp2.0 + $(NetCoreAppVersion) @@ -14,9 +13,7 @@ - - - + + - diff --git a/src/Examples/GettingStarted/Models/Article.cs b/src/Examples/GettingStarted/Models/Article.cs index 68cecf060d..f10c3b175f 100644 --- a/src/Examples/GettingStarted/Models/Article.cs +++ b/src/Examples/GettingStarted/Models/Article.cs @@ -6,7 +6,6 @@ public class Article : Identifiable { [Attr] public string Title { get; set; } - [HasOne] public Person Author { get; set; } public int AuthorId { get; set; } diff --git a/src/Examples/GettingStarted/Models/Person.cs b/src/Examples/GettingStarted/Models/Person.cs index 625cf26ab6..39b59a44bb 100644 --- a/src/Examples/GettingStarted/Models/Person.cs +++ b/src/Examples/GettingStarted/Models/Person.cs @@ -7,7 +7,6 @@ public class Person : Identifiable { [Attr] public string Name { get; set; } - [HasMany] public List
Articles { get; set; } } diff --git a/src/Examples/GettingStarted/Program.cs b/src/Examples/GettingStarted/Program.cs index fdc5046542..d558bead45 100644 --- a/src/Examples/GettingStarted/Program.cs +++ b/src/Examples/GettingStarted/Program.cs @@ -1,26 +1,18 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace GettingStarted -{ - public class Program - { - public static void Main(string[] args) - { - BuildWebHost(args).Run(); - } - - public static IWebHost BuildWebHost(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseStartup() - .UseUrls("http://localhost:5001") - .Build(); - } -} +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; + +namespace GettingStarted +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .UseUrls("http://localhost:5001"); + } +} diff --git a/src/Examples/GettingStarted/Properties/launchSettings.json b/src/Examples/GettingStarted/Properties/launchSettings.json index a0f317edf0..417b2e9f1e 100644 --- a/src/Examples/GettingStarted/Properties/launchSettings.json +++ b/src/Examples/GettingStarted/Properties/launchSettings.json @@ -1,24 +1,12 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:56042/", - "sslPort": 0 - } - }, "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, "GettingStarted": { "commandName": "Project", "launchBrowser": true, - "environmentVariables": {} + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" } } } \ No newline at end of file diff --git a/src/Examples/GettingStarted/ResourceDefinitionExample/ModelDefinition.cs b/src/Examples/GettingStarted/ResourceDefinitionExample/ModelDefinition.cs index e9581fc401..6fd6a131f4 100644 --- a/src/Examples/GettingStarted/ResourceDefinitionExample/ModelDefinition.cs +++ b/src/Examples/GettingStarted/ResourceDefinitionExample/ModelDefinition.cs @@ -1,21 +1,19 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; namespace GettingStarted.ResourceDefinitionExample { public class ModelDefinition : ResourceDefinition { - public ModelDefinition(IResourceGraph graph) : base(graph) + public ModelDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { + // this allows POST / PATCH requests to set the value of a + // property, but we don't include this value in the response + // this might be used if the incoming value gets hashed or + // encrypted prior to being persisted and this value should + // never be sent back to the client + HideFields(model => model.DontExpose); } - - // this allows POST / PATCH requests to set the value of a - // property, but we don't include this value in the response - // this might be used if the incoming value gets hashed or - // encrypted prior to being persisted and this value should - // never be sent back to the client - protected override List OutputAttrs() - => Remove(model => model.DontExpose); } -} \ No newline at end of file +} diff --git a/src/Examples/GettingStarted/ResourceDefinitionExample/ModelsController.cs b/src/Examples/GettingStarted/ResourceDefinitionExample/ModelsController.cs index a14394e830..1b488ed383 100644 --- a/src/Examples/GettingStarted/ResourceDefinitionExample/ModelsController.cs +++ b/src/Examples/GettingStarted/ResourceDefinitionExample/ModelsController.cs @@ -1,4 +1,4 @@ -using GettingStarted.Models; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; @@ -7,9 +7,9 @@ namespace GettingStarted.ResourceDefinitionExample public class ModelsController : JsonApiController { public ModelsController( - IJsonApiContext jsonApiContext, - IResourceService resourceService) - : base(jsonApiContext, resourceService) + IJsonApiOptions jsonApiOptions, + IResourceService resourceService) + : base(jsonApiOptions, resourceService) { } } -} \ No newline at end of file +} diff --git a/src/Examples/GettingStarted/Startup.cs b/src/Examples/GettingStarted/Startup.cs index 5d0fa8dc91..75a4704301 100644 --- a/src/Examples/GettingStarted/Startup.cs +++ b/src/Examples/GettingStarted/Startup.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.EntityFrameworkCore; using JsonApiDotNetCore.Extensions; @@ -20,18 +14,15 @@ public void ConfigureServices(IServiceCollection services) options.UseSqlite("Data Source=sample.db"); }); - var mvcCoreBuilder = services.AddMvcCore(); + var mvcBuilder = services.AddMvcCore(); services.AddJsonApi( options => options.Namespace = "api", - mvcCoreBuilder, - discover => discover.AddCurrentAssembly()); + discover => discover.AddCurrentAssembly(), mvcBuilder: mvcBuilder); } - public void Configure(IApplicationBuilder app, IHostingEnvironment env, SampleDbContext context) + public void Configure(IApplicationBuilder app, SampleDbContext context) { context.Database.EnsureDeleted(); // indicies need to be reset - context.Database.EnsureCreated(); - app.UseJsonApi(); } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs index 95aa7d69f9..faa533093c 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs @@ -1,3 +1,4 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; @@ -7,9 +8,9 @@ namespace JsonApiDotNetCoreExample.Controllers public class ArticlesController : JsonApiController
{ public ArticlesController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, IResourceService
resourceService) - : base(jsonApiContext, resourceService) + : base(jsonApiOptions, resourceService) { } } -} \ No newline at end of file +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs index e46b3f8efd..ee98b7f23d 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs @@ -1,20 +1,18 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCoreExample.Controllers { - [Route("[controller]")] - [DisableRoutingConvention] public class CamelCasedModelsController : JsonApiController { public CamelCasedModelsController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceService, loggerFactory) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs index 28a47eb419..a040ff21e4 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs @@ -1,15 +1,18 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; +using Microsoft.Extensions.Logging; namespace JsonApiDotNetCoreExample.Controllers { public class PassportsController : JsonApiController { - public PassportsController( - IJsonApiContext jsonApiContext, - IResourceService resourceService) - : base(jsonApiContext, resourceService) - { } + public PassportsController(IJsonApiOptions jsonApiOptions, + IResourceService resourceService, + ILoggerFactory loggerFactory = null) + : base(jsonApiOptions, resourceService, loggerFactory) + { + } } -} \ No newline at end of file +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs index e249e2af53..851b2cfc80 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs @@ -1,3 +1,4 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; @@ -8,10 +9,10 @@ namespace JsonApiDotNetCoreExample.Controllers public class PeopleController : JsonApiController { public PeopleController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceService, loggerFactory) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/PersonRolesController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/PersonRolesController.cs index dbc3b482f5..bee457a1cb 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/PersonRolesController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/PersonRolesController.cs @@ -1,3 +1,4 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; @@ -8,10 +9,10 @@ namespace JsonApiDotNetCoreExample.Controllers public class PersonRolesController : JsonApiController { public PersonRolesController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceService, loggerFactory) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs index 6a27038191..d300e24f46 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs @@ -1,8 +1,10 @@ using System; using System.Linq; using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Mvc; @@ -13,18 +15,16 @@ namespace JsonApiDotNetCoreExample.Controllers { public class TodoCollectionsController : JsonApiController { - readonly IDbContextResolver _dbResolver; - public TodoCollectionsController( - IDbContextResolver contextResolver, - IJsonApiContext jsonApiContext, - IResourceService resourceService, - ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + public TodoCollectionsController( + IJsonApiOptions jsonApiOptions, + IDbContextResolver contextResolver, + IResourceService resourceService, + ILoggerFactory loggerFactory) + : base(jsonApiOptions, resourceService, loggerFactory) { _dbResolver = contextResolver; - } [HttpPatch("{id}")] @@ -40,4 +40,4 @@ public override async Task PatchAsync(Guid id, [FromBody] TodoIte } } -} \ No newline at end of file +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs index 768dd1c37c..818e082db9 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs @@ -1,3 +1,4 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; @@ -8,10 +9,10 @@ namespace JsonApiDotNetCoreExample.Controllers public class TodoItemsController : JsonApiController { public TodoItemsController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceService, loggerFactory) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs index ca2e860fa9..c4913689e4 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; @@ -9,15 +10,14 @@ namespace JsonApiDotNetCoreExample.Controllers { - [DisableRoutingConvention] - [Route("custom/route/todo-items")] + [DisableRoutingConvention, Route("custom/route/todo-items")] public class TodoItemsCustomController : CustomJsonApiController { public TodoItemsCustomController( - IJsonApiContext jsonApiContext, + IJsonApiOptions options, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(options, resourceService, loggerFactory) { } } @@ -25,19 +25,20 @@ public class CustomJsonApiController : CustomJsonApiController where T : class, IIdentifiable { public CustomJsonApiController( - IJsonApiContext jsonApiContext, + IJsonApiOptions options, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) - { } + : base(options, resourceService, loggerFactory) + { + } } public class CustomJsonApiController : ControllerBase where T : class, IIdentifiable { private readonly ILogger _logger; + private readonly IJsonApiOptions _options; private readonly IResourceService _resourceService; - private readonly IJsonApiContext _jsonApiContext; protected IActionResult Forbidden() { @@ -45,20 +46,18 @@ protected IActionResult Forbidden() } public CustomJsonApiController( - IJsonApiContext jsonApiContext, + IJsonApiOptions options, IResourceService resourceService, ILoggerFactory loggerFactory) { - _jsonApiContext = jsonApiContext.ApplyContext(this); + _options = options; _resourceService = resourceService; - _logger = loggerFactory.CreateLogger>(); + _logger = loggerFactory.CreateLogger>(); } public CustomJsonApiController( - IJsonApiContext jsonApiContext, IResourceService resourceService) { - _jsonApiContext = jsonApiContext.ApplyContext(this); _resourceService = resourceService; } @@ -103,7 +102,7 @@ public virtual async Task PostAsync([FromBody] T entity) if (entity == null) return UnprocessableEntity(); - if (!_jsonApiContext.Options.AllowClientGeneratedIds && !string.IsNullOrEmpty(entity.StringId)) + if (_options.AllowClientGeneratedIds && !string.IsNullOrEmpty(entity.StringId)) return Forbidden(); entity = await _resourceService.CreateAsync(entity); diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs index 9bab3cf544..971f579b69 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs @@ -1,3 +1,4 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; @@ -11,10 +12,10 @@ public abstract class AbstractTodoItemsController : JsonApiController where T : class, IIdentifiable { protected AbstractTodoItemsController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, IResourceService service, ILoggerFactory loggerFactory) - : base(jsonApiContext, service, loggerFactory) + : base(jsonApiOptions, service, loggerFactory) { } } @@ -22,10 +23,10 @@ protected AbstractTodoItemsController( public class TodoItemsTestController : AbstractTodoItemsController { public TodoItemsTestController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, IResourceService service, ILoggerFactory loggerFactory) - : base(jsonApiContext, service, loggerFactory) + : base(jsonApiOptions, service, loggerFactory) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs index dbd144caa4..cc47e88d84 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs @@ -1,3 +1,4 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; @@ -8,10 +9,10 @@ namespace JsonApiDotNetCoreExample.Controllers public class UsersController : JsonApiController { public UsersController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceService, loggerFactory) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs b/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs index d7147123f6..a1887ba235 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs @@ -1,14 +1,25 @@ using JsonApiDotNetCoreExample.Models; using Microsoft.EntityFrameworkCore; -using JsonApiDotNetCoreExample.Models.Entities; namespace JsonApiDotNetCoreExample.Data { public class AppDbContext : DbContext { - public AppDbContext(DbContextOptions options) - : base(options) - { } + public DbSet TodoItems { get; set; } + public DbSet Passports { get; set; } + public DbSet People { get; set; } + public DbSet TodoItemCollections { get; set; } + public DbSet CamelCasedModels { get; set; } + public DbSet
Articles { get; set; } + public DbSet Authors { get; set; } + public DbSet NonJsonApiResources { get; set; } + public DbSet Users { get; set; } + public DbSet PersonRoles { get; set; } + public DbSet ArticleTags { get; set; } + public DbSet IdentifiableArticleTags { get; set; } + public DbSet Tags { get; set; } + + public AppDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -25,19 +36,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .WithMany(p => p.TodoItems) .HasForeignKey(t => t.OwnerId); - modelBuilder.Entity() - .HasKey(r => new { r.CourseId, r.StudentId }); - - modelBuilder.Entity() - .HasOne(r => r.Course) - .WithMany(c => c.Students) - .HasForeignKey(r => r.CourseId); - - modelBuilder.Entity() - .HasOne(r => r.Student) - .WithMany(s => s.Courses) - .HasForeignKey(r => r.StudentId); - modelBuilder.Entity() .HasKey(bc => new { bc.ArticleId, bc.TagId }); @@ -52,7 +50,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity() .HasOne(t => t.DependentTodoItem); - + modelBuilder.Entity() .HasMany(t => t.ChildrenTodoItems) .WithOne(t => t.ParentTodoItem) @@ -74,23 +72,5 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .WithOne(p => p.ToOnePerson) .HasForeignKey(p => p.ToOnePersonId); } - - public DbSet TodoItems { get; set; } - public DbSet Passports { get; set; } - public DbSet People { get; set; } - public DbSet TodoItemCollections { get; set; } - public DbSet CamelCasedModels { get; set; } - public DbSet
Articles { get; set; } - public DbSet Authors { get; set; } - public DbSet NonJsonApiResources { get; set; } - public DbSet Users { get; set; } - public DbSet Courses { get; set; } - public DbSet Departments { get; set; } - public DbSet Registrations { get; set; } - public DbSet Students { get; set; } - public DbSet PersonRoles { get; set; } - public DbSet ArticleTags { get; set; } - public DbSet IdentifiableArticleTags { get; set; } - public DbSet Tags { get; set; } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj b/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj index d56e91f21e..d67f773ea7 100644 --- a/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj +++ b/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj @@ -5,6 +5,7 @@ JsonApiDotNetCoreExample Exe JsonApiDotNetCoreExample + InProcess @@ -12,9 +13,8 @@ - - - + + @@ -28,4 +28,7 @@ + + + diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Entities/CourseEntity.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Entities/CourseEntity.cs deleted file mode 100644 index a5e2c45f52..0000000000 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Entities/CourseEntity.cs +++ /dev/null @@ -1,31 +0,0 @@ -using JsonApiDotNetCore.Models; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace JsonApiDotNetCoreExample.Models.Entities -{ - [Table("Course")] - public class CourseEntity : Identifiable - { - [Column("number")] - [Required] - public int Number { get; set; } - - [Column("title")] - [Required] - [StringLength(255)] - public string Title { get; set; } - - [Column("description")] - [StringLength(4000)] - public string Description { get; set; } - - public DepartmentEntity Department { get; set; } - - [Column("department_id")] - public int? DepartmentId { get; set; } - - public List Students { get; set; } - } -} diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Entities/CourseStudentEntity.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Entities/CourseStudentEntity.cs deleted file mode 100644 index 3fe23cdc67..0000000000 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Entities/CourseStudentEntity.cs +++ /dev/null @@ -1,50 +0,0 @@ -using JsonApiDotNetCore.Models; -using Microsoft.EntityFrameworkCore.Infrastructure; -using System.ComponentModel.DataAnnotations.Schema; - -namespace JsonApiDotNetCoreExample.Models.Entities -{ - [Table("CourseStudent")] - public class CourseStudentEntity : Identifiable - { - private CourseEntity _course; - private StudentEntity _student; - private ILazyLoader _loader { get; set; } - private CourseStudentEntity(ILazyLoader loader) - { - _loader = loader; - } - - public CourseStudentEntity(int courseId, int studentId) - { - CourseId = courseId; - StudentId = studentId; - } - - public CourseStudentEntity(CourseEntity course, StudentEntity student) - { - Course = course; - CourseId = course.Id; - Student = student; - StudentId = student.Id; - } - - [Column("course_id")] - public int CourseId { get; set; } - - public CourseEntity Course - { - get => _loader.Load(this, ref _course); - set => _course = value; - } - - [Column("student_id")] - public int StudentId { get; set; } - - public StudentEntity Student - { - get => _loader.Load(this, ref _student); - set => _student = value; - } - } -} diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Entities/DepartmentEntity.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Entities/DepartmentEntity.cs deleted file mode 100644 index 337de4279f..0000000000 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Entities/DepartmentEntity.cs +++ /dev/null @@ -1,17 +0,0 @@ -using JsonApiDotNetCore.Models; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace JsonApiDotNetCoreExample.Models.Entities -{ - [Table("Department")] - public class DepartmentEntity : Identifiable - { - [Required] - [StringLength(255, MinimumLength = 3)] - public string Name { get; set; } - - public List Courses { get; set; } - } -} diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Entities/StudentEntity.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Entities/StudentEntity.cs deleted file mode 100644 index 1e23a471c5..0000000000 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Entities/StudentEntity.cs +++ /dev/null @@ -1,25 +0,0 @@ -using JsonApiDotNetCore.Models; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace JsonApiDotNetCoreExample.Models.Entities -{ - [Table("Student")] - public class StudentEntity : Identifiable - { - [Column("firstname")] - [Required] - public string FirstName { get; set; } - - [Column("lastname")] - [Required] - [StringLength(255, MinimumLength = 3)] - public string LastName { get; set; } - - [Column("address")] - public string Address { get; set; } - - public List Courses { get; set; } - } -} diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs index 3b3d44a8e2..3e18bb5feb 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs @@ -1,7 +1,6 @@ -using System; using System.Collections.Generic; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; +using JsonApiDotNetCore.Models.Links; namespace JsonApiDotNetCoreExample.Models { @@ -11,7 +10,7 @@ public class PersonRole : Identifiable public Person Person { get; set; } } - public class Person : Identifiable, IHasMeta, IIsLockable + public class Person : Identifiable, IIsLockable { public bool IsLocked { get; set; } @@ -45,7 +44,7 @@ public class Person : Identifiable, IHasMeta, IIsLockable public virtual TodoItem StakeHolderTodo { get; set; } public virtual int? StakeHolderTodoId { get; set; } - [HasOne("unincludeable-item", documentLinks: Link.All, canInclude: false)] + [HasOne("unincludeable-item", links: Link.All, canInclude: false)] public virtual TodoItem UnIncludeableItem { get; set; } public int? PassportId { get; set; } @@ -53,13 +52,5 @@ public class Person : Identifiable, IHasMeta, IIsLockable [HasOne("passport")] public virtual Passport Passport { get; set; } - public Dictionary GetMeta(IJsonApiContext context) - { - return new Dictionary { - { "copyright", "Copyright 2015 Example Corp." }, - { "authors", new string[] { "Jared Nance" } } - }; - } - } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs deleted file mode 100644 index e981c70cc9..0000000000 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseResource.cs +++ /dev/null @@ -1,28 +0,0 @@ -using JsonApiDotNetCore.Models; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using JsonApiDotNetCoreExample.Models.Entities; - -namespace JsonApiDotNetCoreExample.Models.Resources -{ - public class CourseResource : Identifiable - { - [Attr("number")] - [Required] - public int Number { get; set; } - - [Attr("title")] - [Required] - public string Title { get; set; } - - [Attr("description")] - public string Description { get; set; } - - [HasOne("department", mappedBy: "Department")] - public DepartmentResource Department { get; set; } - public int? DepartmentId { get; set; } - - [HasMany("students")] - public List Students { get; set; } - } -} diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseStudentResource.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseStudentResource.cs deleted file mode 100644 index c6a6619ab8..0000000000 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/CourseStudentResource.cs +++ /dev/null @@ -1,23 +0,0 @@ -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCoreExample.Models.Resources -{ - /// - /// Note: EF Core *requires* the creation of an additional entity - /// for many to many relationships and no longer implicitly creates - /// it. While it may not make sense to create a corresponding "resource" - /// for that relationship, due to the need to make the underlying - /// framework and mapping understand the explicit navigation entity, - /// a mirroring DTO resource is also required. - /// - public class CourseStudentResource : Identifiable - { - [HasOne("course")] - public CourseResource Course { get; set; } - public int CourseId { get; set; } - - [HasOne("student")] - public StudentResource Student { get; set; } - public int StudentId { get; set; } - } -} diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/DepartmentResource.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Resources/DepartmentResource.cs deleted file mode 100644 index 47648afcdf..0000000000 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/DepartmentResource.cs +++ /dev/null @@ -1,14 +0,0 @@ -using JsonApiDotNetCore.Models; -using System.Collections.Generic; - -namespace JsonApiDotNetCoreExample.Models.Resources -{ - public class DepartmentResource : Identifiable - { - [Attr("name")] - public string Name { get; set; } - - [HasMany("courses", mappedBy: "Courses")] - public List Courses { get; set; } - } -} diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/StudentResource.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Resources/StudentResource.cs deleted file mode 100644 index 086d81614b..0000000000 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Resources/StudentResource.cs +++ /dev/null @@ -1,23 +0,0 @@ -using JsonApiDotNetCore.Models; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace JsonApiDotNetCoreExample.Models.Resources -{ - public class StudentResource : Identifiable - { - [Attr("firstname")] - [Required] - public string FirstName { get; set; } - - [Attr("lastname")] - [Required] - public string LastName { get; set; } - - [Attr("address")] - public string Address { get; set; } - - [HasMany("courses")] - public List Courses { get; set; } - } -} diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs b/src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs index 85877b3848..fa17134680 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs @@ -9,12 +9,13 @@ public class TodoItemCollection : Identifiable { [Attr("name")] public string Name { get; set; } - public int OwnerId { get; set; } [HasMany("todo-items")] public virtual List TodoItems { get; set; } [HasOne("owner")] public virtual Person Owner { get; set; } + + public int? OwnerId { get; set; } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/User.cs b/src/Examples/JsonApiDotNetCoreExample/Models/User.cs index 3b66f0dbb2..f966cb84cd 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/User.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/User.cs @@ -1,10 +1,11 @@ +using System; using JsonApiDotNetCore.Models; namespace JsonApiDotNetCoreExample.Models { public class User : Identifiable { - [Attr("username")] public string Username { get; set; } - [Attr("password")] public string Password { get; set; } + [Attr] public string Username { get; set; } + [Attr] public string Password { get; set; } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Program.cs b/src/Examples/JsonApiDotNetCoreExample/Program.cs index b9bbe37b6a..f17228e167 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Program.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Program.cs @@ -5,11 +5,12 @@ namespace JsonApiDotNetCoreExample { public class Program { - public static void Main(string[] args) => BuildWebHost(args).Run(); - - public static IWebHost BuildWebHost(string[] args) => + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) - .UseStartup() - .Build(); + .UseStartup(); } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Properties/launchSettings.json b/src/Examples/JsonApiDotNetCoreExample/Properties/launchSettings.json index 0daa3352d1..fa59af8d9d 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Properties/launchSettings.json +++ b/src/Examples/JsonApiDotNetCoreExample/Properties/launchSettings.json @@ -22,7 +22,8 @@ "launchUrl": "http://localhost:5000/api/values", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "applicationUrl": "http://localhost:5000/" } } } \ No newline at end of file diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/ArticleResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/ArticleResource.cs index 66429f175c..9a36eb27dc 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/ArticleResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/ArticleResource.cs @@ -5,14 +5,13 @@ using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Hooks; using JsonApiDotNetCoreExample.Models; -using Microsoft.Extensions.Logging; -using System.Security.Principal; +using JsonApiDotNetCore.Internal.Contracts; namespace JsonApiDotNetCoreExample.Resources { public class ArticleResource : ResourceDefinition
{ - public ArticleResource(IResourceGraph graph) : base(graph) { } + public ArticleResource(IResourceGraph resourceGraph) : base(resourceGraph) { } public override IEnumerable
OnReturn(HashSet
entities, ResourcePipeline pipeline) { diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResource.cs index 7ad9659f18..c757191304 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResource.cs @@ -1,15 +1,17 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; namespace JsonApiDotNetCoreExample.Resources { public abstract class LockableResource : ResourceDefinition where T : class, IIsLockable, IIdentifiable { - protected LockableResource(IResourceGraph graph) : base(graph) { } + protected LockableResource(IResourceGraph resourceGraph) : base(resourceGraph) { } protected void DisallowLocked(IEnumerable entities) { diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/PassportResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/PassportResource.cs index 457468c484..25cc4afb72 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/PassportResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/PassportResource.cs @@ -5,12 +5,13 @@ using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Hooks; using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCore.Internal.Contracts; namespace JsonApiDotNetCoreExample.Resources { public class PassportResource : ResourceDefinition { - public PassportResource(IResourceGraph graph) : base(graph) + public PassportResource(IResourceGraph resourceGraph) : base(resourceGraph) { } diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/PersonResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/PersonResource.cs index 4887414e73..4c786b238c 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/PersonResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/PersonResource.cs @@ -1,14 +1,20 @@ using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Hooks; using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; namespace JsonApiDotNetCoreExample.Resources { - public class PersonResource : LockableResource + public class PersonResource : LockableResource, IHasMeta { - public PersonResource(IResourceGraph graph) : base(graph) { } + public PersonResource(IResourceGraph resourceGraph) : base(resourceGraph) { } + + public override IEnumerable BeforeUpdate(IDiffableEntityHashSet entities, ResourcePipeline pipeline) + { + return base.BeforeUpdate(entities, pipeline); + } public override IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary entitiesByRelationship, ResourcePipeline pipeline) { @@ -20,5 +26,13 @@ public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary

().ToList().ForEach(kvp => DisallowLocked(kvp.Value)); } + + public Dictionary GetMeta() + { + return new Dictionary { + { "copyright", "Copyright 2015 Example Corp." }, + { "authors", new string[] { "Jared Nance", "Maurits Moeys", "Harro van der Kroft" } } + }; + } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/TagResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/TagResource.cs index e3b3100ddd..1999936e34 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/TagResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/TagResource.cs @@ -4,14 +4,13 @@ using JsonApiDotNetCore.Hooks; using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; namespace JsonApiDotNetCoreExample.Resources { public class TagResource : ResourceDefinition { - public TagResource(IResourceGraph graph) : base(graph) - { - } + public TagResource(IResourceGraph resourceGraph) : base(resourceGraph) { } public override IEnumerable BeforeCreate(IEntityHashSet affected, ResourcePipeline pipeline) { diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/TodoResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/TodoResource.cs index cfba9855d3..26f6c69c64 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/TodoResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/TodoResource.cs @@ -4,12 +4,13 @@ using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Hooks; using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCore.Internal.Contracts; namespace JsonApiDotNetCoreExample.Resources { public class TodoResource : LockableResource { - public TodoResource(IResourceGraph graph) : base(graph) { } + public TodoResource(IResourceGraph resourceGraph) : base(resourceGraph) { } public override void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) { diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs index ec54b6144e..9aa8d8397f 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs @@ -1,18 +1,18 @@ -using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Models; using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCore.Internal.Query; -using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Services; namespace JsonApiDotNetCoreExample.Resources { public class UserResource : ResourceDefinition { - public UserResource(IResourceGraph graph) : base(graph) { } - - protected override List OutputAttrs() - => Remove(user => user.Password); + public UserResource(IResourceGraph resourceGraph) : base(resourceGraph) + { + HideFields(u => u.Password); + } public override QueryFilters GetQueryFilters() { @@ -24,13 +24,15 @@ public override QueryFilters GetQueryFilters() private IQueryable FirstCharacterFilter(IQueryable users, FilterQuery filterQuery) { - switch(filterQuery.Operation) - { - case "lt": - return users.Where(u => u.Username[0] < filterQuery.Value[0]); - default: - return users.Where(u => u.Username[0] == filterQuery.Value[0]); - } + switch (filterQuery.Operation) + { + /// In EF core >= 3.0 we need to explicitly evaluate the query first. This could probably be translated + /// into a query by building expression trees. + case "lt": + return users.ToList().Where(u => u.Username.First() < filterQuery.Value[0]).AsQueryable(); + default: + return users.ToList().Where(u => u.Username.First() == filterQuery.Value[0]).AsQueryable(); + } } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs b/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs new file mode 100644 index 0000000000..d34f32756d --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs @@ -0,0 +1,37 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Hooks; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Query; +using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreExample.Models; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace JsonApiDotNetCoreExample.Services +{ + public class CustomArticleService : DefaultResourceService

+ { + public CustomArticleService(IEnumerable queryParameters, + IJsonApiOptions options, + IResourceRepository repository, + IResourceContextProvider provider, + IResourceHookExecutor hookExecutor = null, + ILoggerFactory loggerFactory = null) + : base(queryParameters, options, repository, provider, hookExecutor, loggerFactory) { } + + public override async Task
GetAsync(int id) + { + var newEntity = await base.GetAsync(id); + if(newEntity == null) + { + throw new JsonApiException(404, "The entity could not be found"); + } + newEntity.Name = "None for you Glen Coco"; + return newEntity; + } + } + +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/ClientGeneratedIdsStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/ClientGeneratedIdsStartup.cs new file mode 100644 index 0000000000..10255d6727 --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/ClientGeneratedIdsStartup.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using JsonApiDotNetCoreExample.Data; +using Microsoft.EntityFrameworkCore; +using JsonApiDotNetCore.Extensions; +using System.Reflection; + +namespace JsonApiDotNetCoreExample +{ + /// + /// This should be in JsonApiDotNetCoreExampleTests project but changes in .net core 3.0 + /// do no longer allow that. See https://github.com/aspnet/AspNetCore/issues/15373. + /// + public class ClientGeneratedIdsStartup : Startup + { + public ClientGeneratedIdsStartup(IWebHostEnvironment env) + : base (env) + { } + + public override void ConfigureServices(IServiceCollection services) + { + var loggerFactory = new LoggerFactory(); + var mvcBuilder = services.AddMvcCore(); + services + .AddSingleton(loggerFactory) + .AddLogging(builder => + { + builder.AddConsole(); + }) + .AddDbContext(options => options.UseNpgsql(GetDbConnectionString()), ServiceLifetime.Transient) + .AddJsonApi(options => { + options.Namespace = "api/v1"; + options.DefaultPageSize = 5; + options.IncludeTotalRecordCount = true; + options.EnableResourceHooks = true; + options.LoaDatabaseValues = true; + options.AllowClientGeneratedIds = true; + }, + discovery => discovery.AddAssembly(Assembly.Load(nameof(JsonApiDotNetCoreExample))), + mvcBuilder: mvcBuilder); + } + } +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/MetaStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/MetaStartup.cs new file mode 100644 index 0000000000..32accb087a --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/MetaStartup.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using JsonApiDotNetCore.Services; +using System.Collections.Generic; + +namespace JsonApiDotNetCoreExample +{ + /// + /// This should be in JsonApiDotNetCoreExampleTests project but changes in .net core 3.0 + /// do no longer allow that. See https://github.com/aspnet/AspNetCore/issues/15373. + /// + public class MetaStartup : Startup + { + public MetaStartup(IWebHostEnvironment env) + : base (env) + { } + + public override void ConfigureServices(IServiceCollection services) + { + services.AddScoped(); + base.ConfigureServices(services); + } + } + + public class MetaService : IRequestMeta + { + public Dictionary GetMeta() + { + return new Dictionary { + { "request-meta", "request-meta-value" } + }; + } + } +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs similarity index 68% rename from src/Examples/JsonApiDotNetCoreExample/Startup.cs rename to src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs index 44784f7eac..3e9d3ca9e3 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs @@ -14,50 +14,49 @@ public class Startup { public readonly IConfiguration Config; - public Startup(IHostingEnvironment env) + public Startup(IWebHostEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) .AddEnvironmentVariables(); - Config = builder.Build(); } - public virtual IServiceProvider ConfigureServices(IServiceCollection services) + public virtual void ConfigureServices(IServiceCollection services) { var loggerFactory = new LoggerFactory(); - loggerFactory.AddConsole(LogLevel.Warning); - - var mvcBuilder = services.AddMvcCore(); - services .AddSingleton(loggerFactory) - .AddDbContext(options => options.UseNpgsql(GetDbConnectionString()), ServiceLifetime.Transient) - .AddJsonApi(options => { + .AddLogging(builder => + { + builder.AddConsole(); + builder.AddConfiguration(Config.GetSection("Logging")); + }) + .AddDbContext(options => + { + options.UseNpgsql(GetDbConnectionString(), options => options.SetPostgresVersion(new Version(9,6))); + }, ServiceLifetime.Transient) + .AddJsonApi(options => + { options.Namespace = "api/v1"; options.DefaultPageSize = 5; options.IncludeTotalRecordCount = true; options.EnableResourceHooks = true; - options.LoadDatabaseValues = true; - }, - mvcBuilder, + options.LoaDatabaseValues = true; + }, discovery => discovery.AddCurrentAssembly()); - - return services.BuildServiceProvider(); + services.AddClientSerialization(); } public virtual void Configure( IApplicationBuilder app, - IHostingEnvironment env, ILoggerFactory loggerFactory, AppDbContext context) { - context.Database.EnsureCreated(); - - loggerFactory.AddConsole(Config.GetSection("Logging")); + context.Database.EnsureCreated(); app.UseJsonApi(); } diff --git a/src/Examples/JsonApiDotNetCoreExample/web.config b/src/Examples/JsonApiDotNetCoreExample/web.config index a8d6672758..50d0b02786 100644 --- a/src/Examples/JsonApiDotNetCoreExample/web.config +++ b/src/Examples/JsonApiDotNetCoreExample/web.config @@ -7,7 +7,7 @@ - + diff --git a/src/Examples/NoEntityFrameworkExample/Controllers/CustomTodoItemsController.cs b/src/Examples/NoEntityFrameworkExample/Controllers/CustomTodoItemsController.cs deleted file mode 100644 index a6ded9749f..0000000000 --- a/src/Examples/NoEntityFrameworkExample/Controllers/CustomTodoItemsController.cs +++ /dev/null @@ -1,17 +0,0 @@ -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExample.Models; -using Microsoft.Extensions.Logging; - -namespace NoEntityFrameworkExample.Controllers -{ - public class CustomTodoItemsController : JsonApiController - { - public CustomTodoItemsController( - IJsonApiContext jsonApiContext, - IResourceService resourceService, - ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) - { } - } -} diff --git a/src/Examples/NoEntityFrameworkExample/Controllers/TodoItemsController.cs b/src/Examples/NoEntityFrameworkExample/Controllers/TodoItemsController.cs new file mode 100644 index 0000000000..cf18987700 --- /dev/null +++ b/src/Examples/NoEntityFrameworkExample/Controllers/TodoItemsController.cs @@ -0,0 +1,18 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using NoEntityFrameworkExample.Models; +using Microsoft.Extensions.Logging; + +namespace NoEntityFrameworkExample.Controllers +{ + public class TodoItemsController : JsonApiController + { + public TodoItemsController( + IJsonApiOptions jsonApiOptions, + IResourceService resourceService, + ILoggerFactory loggerFactory) + : base(jsonApiOptions, resourceService, loggerFactory) + { } + } +} diff --git a/src/Examples/NoEntityFrameworkExample/Data/AppDbContext.cs b/src/Examples/NoEntityFrameworkExample/Data/AppDbContext.cs new file mode 100644 index 0000000000..e7247108dd --- /dev/null +++ b/src/Examples/NoEntityFrameworkExample/Data/AppDbContext.cs @@ -0,0 +1,14 @@ +using NoEntityFrameworkExample.Models; +using Microsoft.EntityFrameworkCore; + +namespace NoEntityFrameworkExample.Data +{ + public class AppDbContext : DbContext + { + public AppDbContext(DbContextOptions options) + : base(options) + { } + + public DbSet TodoItems { get; set; } + } +} diff --git a/src/Examples/NoEntityFrameworkExample/Models/TodoItem.cs b/src/Examples/NoEntityFrameworkExample/Models/TodoItem.cs new file mode 100644 index 0000000000..b1021d18f5 --- /dev/null +++ b/src/Examples/NoEntityFrameworkExample/Models/TodoItem.cs @@ -0,0 +1,36 @@ +using System; +using JsonApiDotNetCore.Models; + +namespace NoEntityFrameworkExample.Models +{ + public class TodoItem : Identifiable + { + public TodoItem() + { + GuidProperty = Guid.NewGuid(); + } + + public bool IsLocked { get; set; } + + [Attr("description")] + public string Description { get; set; } + + [Attr("ordinal")] + public long Ordinal { get; set; } + + [Attr("guid-property")] + public Guid GuidProperty { get; set; } + + [Attr("created-date")] + public DateTime CreatedDate { get; set; } + + [Attr("achieved-date", isFilterable: false, isSortable: false)] + public DateTime? AchievedDate { get; set; } + + [Attr("updated-date")] + public DateTime? UpdatedDate { get; set; } + + [Attr("offset-date")] + public DateTimeOffset? OffsetDate { get; set; } + } +} diff --git a/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj b/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj old mode 100755 new mode 100644 index efdaa68e5b..b387f93746 --- a/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj +++ b/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj @@ -1,24 +1,15 @@ - + $(NetCoreAppVersion) + InProcess - - - - - - - - - - diff --git a/src/Examples/NoEntityFrameworkExample/Program.cs b/src/Examples/NoEntityFrameworkExample/Program.cs index 76f3020c52..9cd9c3ce22 100755 --- a/src/Examples/NoEntityFrameworkExample/Program.cs +++ b/src/Examples/NoEntityFrameworkExample/Program.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore; +using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; namespace NoEntityFrameworkExample @@ -7,12 +7,10 @@ public class Program { public static void Main(string[] args) { - BuildWebHost(args).Run(); + CreateWebHostBuilder(args).Build().Run(); } - - public static IWebHost BuildWebHost(string[] args) => + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) - .UseStartup() - .Build(); + .UseStartup(); } } diff --git a/src/Examples/NoEntityFrameworkExample/Properties/launchSettings.json b/src/Examples/NoEntityFrameworkExample/Properties/launchSettings.json index 310f04da95..1dff6cfe69 100644 --- a/src/Examples/NoEntityFrameworkExample/Properties/launchSettings.json +++ b/src/Examples/NoEntityFrameworkExample/Properties/launchSettings.json @@ -8,16 +8,20 @@ } }, "profiles": { + "NoEntityFrameworkExample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:5000/" + }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } - }, - "NoEntityFrameworkExample": { - "commandName": "Project", - "environmentVariables": {} } } } \ No newline at end of file diff --git a/src/Examples/NoEntityFrameworkExample/Services/TodoItemService.cs b/src/Examples/NoEntityFrameworkExample/Services/TodoItemService.cs index f68c056829..09078cda2c 100644 --- a/src/Examples/NoEntityFrameworkExample/Services/TodoItemService.cs +++ b/src/Examples/NoEntityFrameworkExample/Services/TodoItemService.cs @@ -1,13 +1,12 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.Extensions.Configuration; using Npgsql; using Dapper; using System.Data; -using JsonApiDotNetCoreExample.Models; +using NoEntityFrameworkExample.Models; using System.Linq; namespace NoEntityFrameworkExample.Services @@ -20,7 +19,7 @@ public TodoItemService(IConfiguration config) { _connectionString = config.GetValue("Data:DefaultConnection"); } - + private IDbConnection Connection { get @@ -59,7 +58,7 @@ public Task GetRelationshipAsync(int id, string relationshipName) throw new NotImplementedException(); } - public Task GetRelationshipsAsync(int id, string relationshipName) + public Task GetRelationshipsAsync(int id, string relationshipName) { throw new NotImplementedException(); } @@ -84,7 +83,7 @@ public Task UpdateAsync(int id, TodoItem entity) throw new NotImplementedException(); } - public Task UpdateRelationshipsAsync(int id, string relationshipName, List relationships) + public Task UpdateRelationshipsAsync(int id, string relationshipName, object relationships) { throw new NotImplementedException(); } diff --git a/src/Examples/NoEntityFrameworkExample/Startup.cs b/src/Examples/NoEntityFrameworkExample/Startup.cs old mode 100755 new mode 100644 index d42c44fa42..1d6aef07c5 --- a/src/Examples/NoEntityFrameworkExample/Startup.cs +++ b/src/Examples/NoEntityFrameworkExample/Startup.cs @@ -1,7 +1,5 @@ using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExample.Data; -using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -9,13 +7,14 @@ using Microsoft.Extensions.Logging; using NoEntityFrameworkExample.Services; using Microsoft.EntityFrameworkCore; -using System; +using NoEntityFrameworkExample.Data; +using NoEntityFrameworkExample.Models; namespace NoEntityFrameworkExample { public class Startup { - public Startup(IHostingEnvironment env) + public Startup(IWebHostEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) @@ -28,37 +27,34 @@ public Startup(IHostingEnvironment env) public IConfigurationRoot Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. - public virtual IServiceProvider ConfigureServices(IServiceCollection services) + public virtual void ConfigureServices(IServiceCollection services) { // Add framework services. var mvcBuilder = services.AddMvcCore(); - - services.AddJsonApi(options => { - options.Namespace = "api/v1"; - options.BuildResourceGraph((builder) => { - builder.AddResource("custom-todo-items"); - }); - }, mvcBuilder); - + services.AddLogging(builder => + { + builder.AddConfiguration(Configuration.GetSection("Logging")); + builder.AddConsole(); + }).AddJsonApi( + options => options.Namespace = "api/v1", + resources: resources => resources.AddResource("todo-items"), + mvcBuilder: mvcBuilder + ); services.AddScoped, TodoItemService>(); - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql(Configuration.GetValue("Data:DefaultConnection")); + optionsBuilder.UseNpgsql(GetDbConnectionString()); services.AddSingleton(Configuration); - services.AddSingleton>(optionsBuilder.Options); + services.AddSingleton(optionsBuilder.Options); services.AddScoped(); - - return services.BuildServiceProvider(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, AppDbContext context) + public void Configure(IApplicationBuilder app, AppDbContext context) { - loggerFactory.AddConsole(Configuration.GetSection("Logging")); - context.Database.EnsureCreated(); - - app.UseMvc(); + app.UseJsonApi(); } + + public string GetDbConnectionString() => Configuration["Data:DefaultConnection"]; } } diff --git a/src/Examples/NoEntityFrameworkExample/appsettings.json b/src/Examples/NoEntityFrameworkExample/appsettings.json old mode 100755 new mode 100644 index 42da2105cc..ed7d5999d7 --- a/src/Examples/NoEntityFrameworkExample/appsettings.json +++ b/src/Examples/NoEntityFrameworkExample/appsettings.json @@ -1,11 +1,11 @@ { - "Data": { - "DefaultConnection": "Host=localhost;Port=5432;Database=JsonApiDotNetCoreExample;User ID=postgres;Password=" - }, - "Logging": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Warning" + "Data": { + "DefaultConnection": "Host=localhost;Port=5432;Database=JsonApiDotNetCoreExample;User ID=postgres;Password=postgres" + }, + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } } - } } diff --git a/src/Examples/OperationsExample/.gitignore b/src/Examples/OperationsExample/.gitignore deleted file mode 100644 index 0f552f400b..0000000000 --- a/src/Examples/OperationsExample/.gitignore +++ /dev/null @@ -1,236 +0,0 @@ -_data/ - -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -build/ -bld/ -[Bb]in/ -[Oo]bj/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Microsoft Azure ApplicationInsights config file -ApplicationInsights.config - -# Windows Store app package directory -AppPackages/ -BundleArtifacts/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe - -# FAKE - F# Make -.fake/ diff --git a/src/Examples/OperationsExample/Controllers/OperationsController.cs b/src/Examples/OperationsExample/Controllers/OperationsController.cs deleted file mode 100644 index 6e56791f9c..0000000000 --- a/src/Examples/OperationsExample/Controllers/OperationsController.cs +++ /dev/null @@ -1,14 +0,0 @@ -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services.Operations; -using Microsoft.AspNetCore.Mvc; - -namespace OperationsExample.Controllers -{ - [Route("api/bulk")] - public class OperationsController : JsonApiOperationsController - { - public OperationsController(IOperationsProcessor processor) - : base(processor) - { } - } -} diff --git a/src/Examples/OperationsExample/OperationsExample.csproj b/src/Examples/OperationsExample/OperationsExample.csproj deleted file mode 100644 index efb3f2b3d4..0000000000 --- a/src/Examples/OperationsExample/OperationsExample.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - $(NetCoreAppVersion) - true - OperationsExample - Exe - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Examples/OperationsExample/Program.cs b/src/Examples/OperationsExample/Program.cs deleted file mode 100644 index 1c2b6b267a..0000000000 --- a/src/Examples/OperationsExample/Program.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; - -namespace OperationsExample -{ - public class Program - { - public static void Main(string[] args) => BuildWebHost(args).Run(); - - public static IWebHost BuildWebHost(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseStartup() - .Build(); - } -} diff --git a/src/Examples/OperationsExample/Startup.cs b/src/Examples/OperationsExample/Startup.cs deleted file mode 100644 index a889ad85d6..0000000000 --- a/src/Examples/OperationsExample/Startup.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCoreExample.Data; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace OperationsExample -{ - public class Startup - { - public readonly IConfiguration Config; - - public Startup(IHostingEnvironment env) - { - var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables(); - - Config = builder.Build(); - } - - public virtual IServiceProvider ConfigureServices(IServiceCollection services) - { - var loggerFactory = new LoggerFactory(); - loggerFactory.AddConsole(LogLevel.Warning); - - services.AddSingleton(loggerFactory); - - services.AddDbContext(options => options.UseNpgsql(GetDbConnectionString()), ServiceLifetime.Scoped); - - services.AddJsonApi(opt => opt.EnableOperations = true); - - return services.BuildServiceProvider(); - } - - public virtual void Configure( - IApplicationBuilder app, - IHostingEnvironment env, - ILoggerFactory loggerFactory, - AppDbContext context) - { - context.Database.EnsureCreated(); - - loggerFactory.AddConsole(Config.GetSection("Logging")); - app.UseJsonApi(); - } - - public string GetDbConnectionString() => Config["Data:DefaultConnection"]; - } -} diff --git a/src/Examples/OperationsExample/appsettings.json b/src/Examples/OperationsExample/appsettings.json deleted file mode 100644 index 73030b1743..0000000000 --- a/src/Examples/OperationsExample/appsettings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "Data": { - "DefaultConnection": "Host=localhost;Port=5432;Database=JsonApiDotNetCoreExample;User ID=postgres;Password=postgres" - }, - "Logging": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Warning", - "System": "Warning", - "Microsoft": "Warning" - } - } -} diff --git a/src/Examples/ReportsExample/Controllers/ReportsController.cs b/src/Examples/ReportsExample/Controllers/ReportsController.cs index 6f431d9291..77099fe380 100644 --- a/src/Examples/ReportsExample/Controllers/ReportsController.cs +++ b/src/Examples/ReportsExample/Controllers/ReportsController.cs @@ -1,17 +1,19 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; - +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal.Contracts; + namespace ReportsExample.Controllers { [Route("api/[controller]")] public class ReportsController : BaseJsonApiController { - public ReportsController( - IJsonApiContext jsonApiContext, + public ReportsController( + IJsonApiOptions jsonApiOptions, IGetAllService getAll) - : base(jsonApiContext, getAll: getAll) + : base(jsonApiOptions, getAll: getAll) { } [HttpGet] diff --git a/src/Examples/ReportsExample/Program.cs b/src/Examples/ReportsExample/Program.cs index f3ce6c81b0..3794a268c4 100644 --- a/src/Examples/ReportsExample/Program.cs +++ b/src/Examples/ReportsExample/Program.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore; +using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; namespace ReportsExample @@ -7,12 +7,10 @@ public class Program { public static void Main(string[] args) { - BuildWebHost(args).Run(); + CreateWebHostBuilder(args).Build().Run(); } - - public static IWebHost BuildWebHost(string[] args) => + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) - .UseStartup() - .Build(); + .UseStartup(); } } diff --git a/src/Examples/ReportsExample/Properties/launchSettings.json b/src/Examples/ReportsExample/Properties/launchSettings.json index 2b84e5d5e8..643dc89799 100644 --- a/src/Examples/ReportsExample/Properties/launchSettings.json +++ b/src/Examples/ReportsExample/Properties/launchSettings.json @@ -8,13 +8,6 @@ } }, "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, "ReportsExample": { "commandName": "Project", "launchBrowser": true, @@ -22,6 +15,13 @@ "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "http://localhost:55654/" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } } } \ No newline at end of file diff --git a/src/Examples/ReportsExample/ReportsExample.csproj b/src/Examples/ReportsExample/ReportsExample.csproj index 24c01b9a8d..ee832bdf7a 100644 --- a/src/Examples/ReportsExample/ReportsExample.csproj +++ b/src/Examples/ReportsExample/ReportsExample.csproj @@ -1,6 +1,7 @@ $(NetCoreAppVersion) + InProcess @@ -12,8 +13,6 @@ - - diff --git a/src/Examples/ReportsExample/Startup.cs b/src/Examples/ReportsExample/Startup.cs index b71b7fa74a..480f2a0f62 100644 --- a/src/Examples/ReportsExample/Startup.cs +++ b/src/Examples/ReportsExample/Startup.cs @@ -11,7 +11,7 @@ public class Startup { public readonly IConfiguration Config; - public Startup(IHostingEnvironment env) + public Startup(IWebHostEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) @@ -27,13 +27,7 @@ public virtual void ConfigureServices(IServiceCollection services) var mvcBuilder = services.AddMvcCore(); services.AddJsonApi( opt => opt.Namespace = "api", - mvcBuilder, - discovery => discovery.AddCurrentAssembly()); - } - - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) - { - app.UseMvc(); + discovery => discovery.AddCurrentAssembly(), mvcBuilder: mvcBuilder); } } } diff --git a/src/Examples/ResourceEntitySeparationExample/Controllers/CoursesController.cs b/src/Examples/ResourceEntitySeparationExample/Controllers/CoursesController.cs deleted file mode 100644 index 6809ace0bb..0000000000 --- a/src/Examples/ResourceEntitySeparationExample/Controllers/CoursesController.cs +++ /dev/null @@ -1,17 +0,0 @@ -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExample.Models.Resources; -using Microsoft.Extensions.Logging; - -namespace ResourceEntitySeparationExample.Controllers -{ - public class CoursesController : JsonApiController - { - public CoursesController( - IJsonApiContext jsonApiContext, - IResourceService resourceService, - ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) - { } - } -} diff --git a/src/Examples/ResourceEntitySeparationExample/Controllers/DepartmentsController.cs b/src/Examples/ResourceEntitySeparationExample/Controllers/DepartmentsController.cs deleted file mode 100644 index 08f3ab33ad..0000000000 --- a/src/Examples/ResourceEntitySeparationExample/Controllers/DepartmentsController.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExample.Models.Resources; -using Microsoft.Extensions.Logging; - -namespace ResourceEntitySeparationExample.Controllers -{ - public class DepartmentsController : JsonApiController - { - public DepartmentsController( - IJsonApiContext jsonApiContext, - IResourceService resourceService, - ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) - { } - } -} diff --git a/src/Examples/ResourceEntitySeparationExample/Controllers/StudentsController.cs b/src/Examples/ResourceEntitySeparationExample/Controllers/StudentsController.cs deleted file mode 100644 index 34d5d33031..0000000000 --- a/src/Examples/ResourceEntitySeparationExample/Controllers/StudentsController.cs +++ /dev/null @@ -1,17 +0,0 @@ -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExample.Models.Resources; -using Microsoft.Extensions.Logging; - -namespace ResourceEntitySeparationExample.Controllers -{ - public class StudentsController : JsonApiController - { - public StudentsController( - IJsonApiContext jsonApiContext, - IResourceService resourceService, - ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) - { } - } -} diff --git a/src/Examples/ResourceEntitySeparationExample/Models/AutoMapperAdapter.cs b/src/Examples/ResourceEntitySeparationExample/Models/AutoMapperAdapter.cs deleted file mode 100644 index 732b45babb..0000000000 --- a/src/Examples/ResourceEntitySeparationExample/Models/AutoMapperAdapter.cs +++ /dev/null @@ -1,25 +0,0 @@ -using AutoMapper; -using JsonApiDotNetCore.Models; - -namespace ResourceEntitySeparationExample.Models -{ - public class AutoMapperAdapter : IResourceMapper - { - private readonly IMapper _mapper; - - public AutoMapperAdapter(IMapper mapper) - { - _mapper = mapper; - } - - public TDestination Map(object source) - { - return _mapper.Map(source); - } - - public TDestination Map(TSource source) - { - return _mapper.Map(source); - } - } -} diff --git a/src/Examples/ResourceEntitySeparationExample/Profiles/CourseProfile.cs b/src/Examples/ResourceEntitySeparationExample/Profiles/CourseProfile.cs deleted file mode 100644 index cc817045af..0000000000 --- a/src/Examples/ResourceEntitySeparationExample/Profiles/CourseProfile.cs +++ /dev/null @@ -1,40 +0,0 @@ -using AutoMapper; -using JsonApiDotNetCoreExample.Models.Entities; -using JsonApiDotNetCoreExample.Models.Resources; -using System.Collections.Generic; - -namespace ResourceEntitySeparationExample.Profiles -{ - public class CourseProfile : Profile - { - public CourseProfile() - { - CreateMap() - .ForMember(r => r.Students, opt => opt.MapFrom(e => StudentsFromRegistrations(e.Students))) - .ForMember(r => r.Department, opt => opt.MapFrom(e => new DepartmentResource - { - Id = e.Department.Id, - Name = e.Department.Name - })); - - CreateMap(); - } - - private ICollection StudentsFromRegistrations(ICollection registrations) - { - ICollection students = new HashSet(); - foreach(CourseStudentEntity reg in registrations) - { - StudentEntity e = reg.Student; - students.Add(new StudentResource - { - Id = e.Id, - FirstName = e.FirstName, - LastName = e.LastName, - Address = e.Address - }); - } - return students.Count == 0 ? null : students; - } - } -} diff --git a/src/Examples/ResourceEntitySeparationExample/Profiles/DepartmentProfile.cs b/src/Examples/ResourceEntitySeparationExample/Profiles/DepartmentProfile.cs deleted file mode 100644 index c8f26fd125..0000000000 --- a/src/Examples/ResourceEntitySeparationExample/Profiles/DepartmentProfile.cs +++ /dev/null @@ -1,14 +0,0 @@ -using AutoMapper; -using JsonApiDotNetCoreExample.Models.Entities; -using JsonApiDotNetCoreExample.Models.Resources; - -namespace ResourceEntitySeparationExample.Profiles -{ - public class DepartmentProfile : Profile - { - public DepartmentProfile() - { - CreateMap(); - } - } -} diff --git a/src/Examples/ResourceEntitySeparationExample/Profiles/StudentProfile.cs b/src/Examples/ResourceEntitySeparationExample/Profiles/StudentProfile.cs deleted file mode 100644 index 160f64c79a..0000000000 --- a/src/Examples/ResourceEntitySeparationExample/Profiles/StudentProfile.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using AutoMapper; -using JsonApiDotNetCoreExample.Models.Entities; -using JsonApiDotNetCoreExample.Models.Resources; - -namespace ResourceEntitySeparationExample.Profiles -{ - public class StudentProfile : Profile - { - public StudentProfile() - { - CreateMap() - .ForMember(d => d.Courses, opt => opt.MapFrom(e => CoursesFromRegistrations(e.Courses))); - - CreateMap(); - } - - private ICollection CoursesFromRegistrations(ICollection registrations) - { - ICollection courses = new HashSet(); - foreach (CourseStudentEntity reg in registrations) - { - CourseEntity e = reg.Course; - courses.Add(new CourseResource - { - Id = e.Id, - Number = e.Number, - Title = e.Title, - Description = e.Description - }); - } - return courses.Count == 0 ? null : courses; - } - } -} diff --git a/src/Examples/ResourceEntitySeparationExample/Program.cs b/src/Examples/ResourceEntitySeparationExample/Program.cs deleted file mode 100644 index 3af82b653a..0000000000 --- a/src/Examples/ResourceEntitySeparationExample/Program.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; - -namespace ResourceEntitySeparationExample -{ - public class Program - { - public static void Main(string[] args) => BuildWebHost(args).Run(); - - public static IWebHost BuildWebHost(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseStartup() - .Build(); - } -} diff --git a/src/Examples/ResourceEntitySeparationExample/Properties/launchSettings.json b/src/Examples/ResourceEntitySeparationExample/Properties/launchSettings.json deleted file mode 100644 index a51fc0dc79..0000000000 --- a/src/Examples/ResourceEntitySeparationExample/Properties/launchSettings.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:57181/", - "sslPort": 0 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "api/v1/students", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "ResourceEntitySeparationExample": { - "commandName": "Project", - "environmentVariables": {} - } - } -} \ No newline at end of file diff --git a/src/Examples/ResourceEntitySeparationExample/ResourceEntitySeparationExample.csproj b/src/Examples/ResourceEntitySeparationExample/ResourceEntitySeparationExample.csproj deleted file mode 100644 index fa6088fa63..0000000000 --- a/src/Examples/ResourceEntitySeparationExample/ResourceEntitySeparationExample.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - netcoreapp2.0 - - - - - - - - - - - - diff --git a/src/Examples/ResourceEntitySeparationExample/Startup.cs b/src/Examples/ResourceEntitySeparationExample/Startup.cs deleted file mode 100644 index a99febfee8..0000000000 --- a/src/Examples/ResourceEntitySeparationExample/Startup.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using AutoMapper; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExample.Data; -using JsonApiDotNetCoreExample.Models.Entities; -using JsonApiDotNetCoreExample.Models.Resources; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using ResourceEntitySeparationExample.Models; - -namespace ResourceEntitySeparationExample -{ - public class Startup - { - public readonly IConfiguration Config; - - public Startup(IHostingEnvironment env) - { - var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) - .AddEnvironmentVariables(); - - Config = builder.Build(); - } - - public virtual IServiceProvider ConfigureServices(IServiceCollection services) - { - var loggerFactory = new LoggerFactory(); - loggerFactory.AddConsole(LogLevel.Warning); - services.AddSingleton(loggerFactory); - - services.AddDbContext(options => options - .UseNpgsql(GetDbConnectionString()), - ServiceLifetime.Transient); - services.AddScoped>(); - - var mvcBuilder = services.AddMvcCore(); - - services.AddJsonApi(options => { - options.Namespace = "api/v1"; - options.DefaultPageSize = 10; - options.IncludeTotalRecordCount = true; - options.BuildResourceGraph((builder) => { - builder.AddResource("courses"); - builder.AddResource("departments"); - builder.AddResource("students"); - }); - }, mvcBuilder); - - services.AddAutoMapper(); - services.AddScoped(); - - services.AddScoped, EntityResourceService>(); - services.AddScoped, EntityResourceService>(); - services.AddScoped, EntityResourceService>(); - - var provider = services.BuildServiceProvider(); - var appContext = provider.GetRequiredService(); - if (appContext == null) - throw new ArgumentException(); - - return provider; - } - - public virtual void Configure( - IApplicationBuilder app, - IHostingEnvironment env, - ILoggerFactory loggerFactory, - AppDbContext context) - { - context.Database.EnsureCreated(); - loggerFactory.AddConsole(Config.GetSection("Logging")); - app.UseJsonApi(); - } - - public string GetDbConnectionString() => Config["Data:DefaultConnection"]; - } -} diff --git a/src/Examples/ResourceEntitySeparationExample/appsettings.Development.json b/src/Examples/ResourceEntitySeparationExample/appsettings.Development.json deleted file mode 100644 index e203e9407e..0000000000 --- a/src/Examples/ResourceEntitySeparationExample/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - } -} diff --git a/src/Examples/ResourceEntitySeparationExample/appsettings.json b/src/Examples/ResourceEntitySeparationExample/appsettings.json deleted file mode 100644 index dc8fc4fae6..0000000000 --- a/src/Examples/ResourceEntitySeparationExample/appsettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Data": { - "DefaultConnection": "Host=localhost;Port=5432;Database=JsonApiDotNetCoreExample;User ID=postgres;Password=postgres" - }, - "Logging": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Warning", - "Microsoft.EntityFrameworkCore.Database.Command": "Information" - } - } -} diff --git a/src/JsonApiDotNetCore/AssemblyInfo.cs b/src/JsonApiDotNetCore/AssemblyInfo.cs index dc8b9ba84c..6fa08b113d 100644 --- a/src/JsonApiDotNetCore/AssemblyInfo.cs +++ b/src/JsonApiDotNetCore/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("OperationsExampleTests")] [assembly:InternalsVisibleTo("UnitTests")] [assembly:InternalsVisibleTo("JsonApiDotNetCoreExampleTests")] [assembly:InternalsVisibleTo("NoEntityFrameworkTests")] diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs deleted file mode 100644 index ad52aeb8bb..0000000000 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ /dev/null @@ -1,354 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Builders -{ - /// - public class DocumentBuilder : IDocumentBuilder - { - private readonly IJsonApiContext _jsonApiContext; - private readonly IResourceGraph _resourceGraph; - private readonly IRequestMeta _requestMeta; - private readonly DocumentBuilderOptions _documentBuilderOptions; - private readonly IScopedServiceProvider _scopedServiceProvider; - - public DocumentBuilder( - IJsonApiContext jsonApiContext, - IRequestMeta requestMeta = null, - IDocumentBuilderOptionsProvider documentBuilderOptionsProvider = null, - IScopedServiceProvider scopedServiceProvider = null) - { - _jsonApiContext = jsonApiContext; - _resourceGraph = jsonApiContext.ResourceGraph; - _requestMeta = requestMeta; - _documentBuilderOptions = documentBuilderOptionsProvider?.GetDocumentBuilderOptions() ?? new DocumentBuilderOptions(); - _scopedServiceProvider = scopedServiceProvider; - } - - /// - public Document Build(IIdentifiable entity) - { - var contextEntity = _resourceGraph.GetContextEntity(entity.GetType()); - - var resourceDefinition = _scopedServiceProvider?.GetService(contextEntity.ResourceType) as IResourceDefinition; - var document = new Document - { - Data = GetData(contextEntity, entity, resourceDefinition), - Meta = GetMeta(entity) - }; - - if (ShouldIncludePageLinks(contextEntity)) - document.Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext)); - - document.Included = AppendIncludedObject(document.Included, contextEntity, entity); - - return document; - } - - /// - public Documents Build(IEnumerable entities) - { - var entityType = entities.GetElementType(); - var contextEntity = _resourceGraph.GetContextEntity(entityType); - var resourceDefinition = _scopedServiceProvider?.GetService(contextEntity.ResourceType) as IResourceDefinition; - - var enumeratedEntities = entities as IList ?? entities.ToList(); - var documents = new Documents - { - Data = new List(), - Meta = GetMeta(enumeratedEntities.FirstOrDefault()) - }; - - if (ShouldIncludePageLinks(contextEntity)) - documents.Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext)); - - foreach (var entity in enumeratedEntities) - { - documents.Data.Add(GetData(contextEntity, entity, resourceDefinition)); - documents.Included = AppendIncludedObject(documents.Included, contextEntity, entity); - } - - return documents; - } - - private Dictionary GetMeta(IIdentifiable entity) - { - var builder = _jsonApiContext.MetaBuilder; - if (_jsonApiContext.Options.IncludeTotalRecordCount && _jsonApiContext.PageManager.TotalRecords != null) - builder.Add("total-records", _jsonApiContext.PageManager.TotalRecords); - - if (_requestMeta != null) - builder.Add(_requestMeta.GetMeta()); - - if (entity != null && entity is IHasMeta metaEntity) - builder.Add(metaEntity.GetMeta(_jsonApiContext)); - - var meta = builder.Build(); - if (meta.Count > 0) - return meta; - - return null; - } - - private bool ShouldIncludePageLinks(ContextEntity entity) => entity.Links.HasFlag(Link.Paging); - - private List AppendIncludedObject(List includedObject, ContextEntity contextEntity, IIdentifiable entity) - { - var includedEntities = GetIncludedEntities(includedObject, contextEntity, entity); - if (includedEntities?.Count > 0) - { - includedObject = includedEntities; - } - - return includedObject; - } - - [Obsolete("You should specify an IResourceDefinition implementation using the GetData/3 overload.")] - public ResourceObject GetData(ContextEntity contextEntity, IIdentifiable entity) - => GetData(contextEntity, entity, resourceDefinition: null); - - /// - public ResourceObject GetData(ContextEntity contextEntity, IIdentifiable entity, IResourceDefinition resourceDefinition = null) - { - var data = new ResourceObject - { - Type = contextEntity.EntityName, - Id = entity.StringId - }; - - if (_jsonApiContext.IsRelationshipPath) - return data; - - data.Attributes = new Dictionary(); - - var resourceAttributes = resourceDefinition?.GetOutputAttrs(entity) ?? contextEntity.Attributes; - resourceAttributes.ForEach(attr => - { - var attributeValue = attr.GetValue(entity); - if (ShouldIncludeAttribute(attr, attributeValue)) - { - data.Attributes.Add(attr.PublicAttributeName, attributeValue); - } - }); - - if (contextEntity.Relationships.Count > 0) - AddRelationships(data, contextEntity, entity); - - return data; - } - private bool ShouldIncludeAttribute(AttrAttribute attr, object attributeValue, RelationshipAttribute relationship = null) - { - return OmitNullValuedAttribute(attr, attributeValue) == false - && attr.InternalAttributeName != nameof(Identifiable.Id) - && ((_jsonApiContext.QuerySet == null - || _jsonApiContext.QuerySet.Fields.Count == 0) - || _jsonApiContext.QuerySet.Fields.Contains(relationship != null ? - $"{relationship.InternalRelationshipName}.{attr.InternalAttributeName}" : - attr.InternalAttributeName)); - } - - private bool OmitNullValuedAttribute(AttrAttribute attr, object attributeValue) - { - return attributeValue == null && _documentBuilderOptions.OmitNullValuedAttributes; - } - - private void AddRelationships(ResourceObject data, ContextEntity contextEntity, IIdentifiable entity) - { - data.Relationships = new Dictionary(); - contextEntity.Relationships.ForEach(r => - data.Relationships.Add( - r.PublicRelationshipName, - GetRelationshipData(r, contextEntity, entity) - ) - ); - } - - private RelationshipData GetRelationshipData(RelationshipAttribute attr, ContextEntity contextEntity, IIdentifiable entity) - { - var linkBuilder = new LinkBuilder(_jsonApiContext); - - var relationshipData = new RelationshipData(); - - if (_jsonApiContext.Options.DefaultRelationshipLinks.HasFlag(Link.None) == false && attr.DocumentLinks.HasFlag(Link.None) == false) - { - relationshipData.Links = new Links(); - if (attr.DocumentLinks.HasFlag(Link.Self)) - relationshipData.Links.Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.StringId, attr.PublicRelationshipName); - - if (attr.DocumentLinks.HasFlag(Link.Related)) - relationshipData.Links.Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.StringId, attr.PublicRelationshipName); - } - - // this only includes the navigation property, we need to actually check the navigation property Id - var navigationEntity = _jsonApiContext.ResourceGraph.GetRelationshipValue(entity, attr); - if (navigationEntity == null) - relationshipData.SingleData = attr.IsHasOne - ? GetIndependentRelationshipIdentifier((HasOneAttribute)attr, entity) - : null; - else if (navigationEntity is IEnumerable) - relationshipData.ManyData = GetRelationships((IEnumerable)navigationEntity); - else - relationshipData.SingleData = GetRelationship(navigationEntity); - - return relationshipData; - } - - private List GetIncludedEntities(List included, ContextEntity rootContextEntity, IIdentifiable rootResource) - { - if (_jsonApiContext.IncludedRelationships != null) - { - foreach (var relationshipName in _jsonApiContext.IncludedRelationships) - { - var relationshipChain = relationshipName.Split('.'); - - var contextEntity = rootContextEntity; - var entity = rootResource; - included = IncludeRelationshipChain(included, rootContextEntity, rootResource, relationshipChain, 0); - } - } - - return included; - } - - private List IncludeRelationshipChain( - List included, ContextEntity parentEntity, IIdentifiable parentResource, string[] relationshipChain, int relationshipChainIndex) - { - var requestedRelationship = relationshipChain[relationshipChainIndex]; - var relationship = parentEntity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == requestedRelationship); - if(relationship == null) - throw new JsonApiException(400, $"{parentEntity.EntityName} does not contain relationship {requestedRelationship}"); - - var navigationEntity = _jsonApiContext.ResourceGraph.GetRelationshipValue(parentResource, relationship); - if(navigationEntity == null) - return included; - if (navigationEntity is IEnumerable hasManyNavigationEntity) - { - foreach (IIdentifiable includedEntity in hasManyNavigationEntity) - { - included = AddIncludedEntity(included, includedEntity, relationship); - included = IncludeSingleResourceRelationships(included, includedEntity, relationship, relationshipChain, relationshipChainIndex); - } - } - else - { - included = AddIncludedEntity(included, (IIdentifiable)navigationEntity, relationship); - included = IncludeSingleResourceRelationships(included, (IIdentifiable)navigationEntity, relationship, relationshipChain, relationshipChainIndex); - } - - return included; - } - - private List IncludeSingleResourceRelationships( - List included, IIdentifiable navigationEntity, RelationshipAttribute relationship, string[] relationshipChain, int relationshipChainIndex) - { - if (relationshipChainIndex < relationshipChain.Length) - { - var nextContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.DependentType); - var resource = (IIdentifiable)navigationEntity; - // recursive call - if (relationshipChainIndex < relationshipChain.Length - 1) - included = IncludeRelationshipChain(included, nextContextEntity, resource, relationshipChain, relationshipChainIndex + 1); - } - - return included; - } - - - private List AddIncludedEntity(List entities, IIdentifiable entity, RelationshipAttribute relationship) - { - var includedEntity = GetIncludedEntity(entity, relationship); - - if (entities == null) - entities = new List(); - - if (includedEntity != null && entities.Any(doc => - string.Equals(doc.Id, includedEntity.Id) && string.Equals(doc.Type, includedEntity.Type)) == false) - { - entities.Add(includedEntity); - } - - return entities; - } - - private ResourceObject GetIncludedEntity(IIdentifiable entity, RelationshipAttribute relationship) - { - if (entity == null) return null; - - var contextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(entity.GetType()); - var resourceDefinition = _scopedServiceProvider.GetService(contextEntity.ResourceType) as IResourceDefinition; - - var data = GetData(contextEntity, entity, resourceDefinition); - - data.Attributes = new Dictionary(); - - contextEntity.Attributes.ForEach(attr => - { - var attributeValue = attr.GetValue(entity); - if (ShouldIncludeAttribute(attr, attributeValue, relationship)) - { - data.Attributes.Add(attr.PublicAttributeName, attributeValue); - } - }); - - return data; - } - - private List GetRelationships(IEnumerable entities) - { - string typeName = null; - var relationships = new List(); - foreach (var entity in entities) - { - // this method makes the assumption that entities is a homogenous collection - // so, we just lookup the type of the first entity on the graph - // this is better than trying to get it from the generic parameter since it could - // be less specific than what is registered on the graph (e.g. IEnumerable) - typeName = typeName ?? _jsonApiContext.ResourceGraph.GetContextEntity(entity.GetType()).EntityName; - relationships.Add(new ResourceIdentifierObject - { - Type = typeName, - Id = ((IIdentifiable)entity).StringId - }); - } - return relationships; - } - - private ResourceIdentifierObject GetRelationship(object entity) - { - var objType = entity.GetType(); - var contextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(objType); - - if (entity is IIdentifiable identifiableEntity) - return new ResourceIdentifierObject - { - Type = contextEntity.EntityName, - Id = identifiableEntity.StringId - }; - - return null; - } - - private ResourceIdentifierObject GetIndependentRelationshipIdentifier(HasOneAttribute hasOne, IIdentifiable entity) - { - var independentRelationshipIdentifier = hasOne.GetIdentifiablePropertyValue(entity); - if (independentRelationshipIdentifier == null) - return null; - - var relatedContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(hasOne.DependentType); - if (relatedContextEntity == null) // TODO: this should probably be a debug log at minimum - return null; - - return new ResourceIdentifierObject - { - Type = relatedContextEntity.EntityName, - Id = independentRelationshipIdentifier.ToString() - }; - } - } -} diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilderOptions.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilderOptions.cs deleted file mode 100644 index a5b16bdd37..0000000000 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilderOptions.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace JsonApiDotNetCore.Builders -{ - /// - /// Options used to configure how a model gets serialized into - /// a json:api document. - /// - public struct DocumentBuilderOptions - { - /// - /// Do not serialize attributes with null values. - /// - public DocumentBuilderOptions(bool omitNullValuedAttributes = false) - { - this.OmitNullValuedAttributes = omitNullValuedAttributes; - } - - /// - /// Prevent attributes with null values from being included in the response. - /// This type is mostly internal and if you want to enable this behavior, you - /// should do so on the . - /// - /// - /// - /// options.NullAttributeResponseBehavior = new NullAttributeResponseBehavior(true); - /// - /// - public bool OmitNullValuedAttributes { get; private set; } - } -} diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilderOptionsProvider.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilderOptionsProvider.cs deleted file mode 100644 index 37c5d51273..0000000000 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilderOptionsProvider.cs +++ /dev/null @@ -1,30 +0,0 @@ -using JsonApiDotNetCore.Services; -using Microsoft.AspNetCore.Http; - -namespace JsonApiDotNetCore.Builders -{ - public class DocumentBuilderOptionsProvider : IDocumentBuilderOptionsProvider - { - private readonly IJsonApiContext _jsonApiContext; - private readonly IHttpContextAccessor _httpContextAccessor; - - public DocumentBuilderOptionsProvider(IJsonApiContext jsonApiContext, IHttpContextAccessor httpContextAccessor) - { - _jsonApiContext = jsonApiContext; - _httpContextAccessor = httpContextAccessor; - } - - public DocumentBuilderOptions GetDocumentBuilderOptions() - { - var nullAttributeResponseBehaviorConfig = this._jsonApiContext.Options.NullAttributeResponseBehavior; - if (nullAttributeResponseBehaviorConfig.AllowClientOverride && _httpContextAccessor.HttpContext.Request.Query.TryGetValue("omitNullValuedAttributes", out var omitNullValuedAttributesQs)) - { - if (bool.TryParse(omitNullValuedAttributesQs, out var omitNullValuedAttributes)) - { - return new DocumentBuilderOptions(omitNullValuedAttributes); - } - } - return new DocumentBuilderOptions(this._jsonApiContext.Options.NullAttributeResponseBehavior.OmitNullValuedAttributes); - } - } -} diff --git a/src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs deleted file mode 100644 index 45ba096447..0000000000 --- a/src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCore.Builders -{ - public interface IDocumentBuilder - { - /// - /// Builds a json:api document from the provided resource instance. - /// - /// The resource to convert. - Document Build(IIdentifiable entity); - - /// - /// Builds a json:api document from the provided resource instances. - /// - /// The collection of resources to convert. - Documents Build(IEnumerable entities); - - [Obsolete("You should specify an IResourceDefinition implementation using the GetData/3 overload.")] - ResourceObject GetData(ContextEntity contextEntity, IIdentifiable entity); - - /// - /// Create the resource object for the provided resource. - /// - /// The metadata for the resource. - /// The resource instance. - /// - /// The resource definition (optional). This can be used for filtering out attributes - /// that should not be exposed to the client. For example, you might want to limit - /// the exposed attributes based on the authenticated user's role. - /// - ResourceObject GetData(ContextEntity contextEntity, IIdentifiable entity, IResourceDefinition resourceDefinition = null); - } -} diff --git a/src/JsonApiDotNetCore/Builders/IDocumentBuilderOptionsProvider.cs b/src/JsonApiDotNetCore/Builders/IDocumentBuilderOptionsProvider.cs deleted file mode 100644 index fe014bced5..0000000000 --- a/src/JsonApiDotNetCore/Builders/IDocumentBuilderOptionsProvider.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace JsonApiDotNetCore.Builders -{ - public interface IDocumentBuilderOptionsProvider - { - DocumentBuilderOptions GetDocumentBuilderOptions(); - } -} diff --git a/src/JsonApiDotNetCore/Builders/IMetaBuilder.cs b/src/JsonApiDotNetCore/Builders/IMetaBuilder.cs deleted file mode 100644 index bf35b9d210..0000000000 --- a/src/JsonApiDotNetCore/Builders/IMetaBuilder.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; - -namespace JsonApiDotNetCore.Builders -{ - public interface IMetaBuilder - { - void Add(string key, object value); - void Add(Dictionary values); - Dictionary Build(); - } -} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs new file mode 100644 index 0000000000..a9a74cf9a7 --- /dev/null +++ b/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs @@ -0,0 +1,60 @@ +using System; +using JsonApiDotNetCore.Graph; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; +using Microsoft.EntityFrameworkCore; + +namespace JsonApiDotNetCore.Builders +{ + public interface IResourceGraphBuilder + { + /// + /// Construct the + /// + IResourceGraph Build(); + + /// + /// Add a json:api resource + /// + /// The resource model type + /// + /// The pluralized name that should be exposed by the API. + /// If nothing is specified, the configured name formatter will be used. + /// See . + /// + IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable; + + + /// + /// Add a json:api resource + /// + /// The resource model type + /// The resource model identifier type + /// + /// The pluralized name that should be exposed by the API. + /// If nothing is specified, the configured name formatter will be used. + /// See . + /// + IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable; + + /// + /// Add a Json:Api resource + /// + /// The resource model type + /// The resource model identifier type + /// + /// The pluralized name that should be exposed by the API. + /// If nothing is specified, the configured name formatter will be used. + /// See . + /// + IResourceGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName = null); + + /// + /// Add all the models that are part of the provided + /// that also implement + /// + /// The implementation type. + IResourceGraphBuilder AddDbContext() where T : DbContext; + } +} diff --git a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs new file mode 100644 index 0000000000..302aeb831f --- /dev/null +++ b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs @@ -0,0 +1,230 @@ +using System; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Formatters; +using JsonApiDotNetCore.Graph; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Generics; +using JsonApiDotNetCore.Managers; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Hooks; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Query; +using JsonApiDotNetCore.Serialization.Server.Builders; +using JsonApiDotNetCore.Serialization.Server; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace JsonApiDotNetCore.Builders +{ + /// + /// A utility class that builds a JsonApi application. It registers all required services + /// and allows the user to override parts of the startup configuration. + /// + public class JsonApiApplicationBuilder + { + public readonly JsonApiOptions JsonApiOptions = new JsonApiOptions(); + private IResourceGraphBuilder _resourceGraphBuilder; + private IServiceDiscoveryFacade _serviceDiscoveryFacade; + private bool _usesDbContext; + private readonly IServiceCollection _services; + private readonly IMvcCoreBuilder _mvcBuilder; + + public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mvcBuilder) + { + _services = services; + _mvcBuilder = mvcBuilder; + } + + internal void ConfigureLogging() + { + _services.AddLogging(); + } + + /// + /// Executes the action provided by the user to configure + /// + public void ConfigureJsonApiOptions(Action configureOptions) => configureOptions(JsonApiOptions); + + /// + /// Configures built-in .net core MVC (things like middleware, routing). Most of this configuration can be adjusted for the developers need. + /// Before calling .AddJsonApi(), a developer can register their own implementation of the following services to customize startup: + /// , , , + /// , and . + /// + public void ConfigureMvc() + { + RegisterJsonApiStartupServices(); + + var intermediateProvider = _services.BuildServiceProvider(); + _resourceGraphBuilder = intermediateProvider.GetRequiredService(); + _serviceDiscoveryFacade = intermediateProvider.GetRequiredService(); + var exceptionFilterProvider = intermediateProvider.GetRequiredService(); + var typeMatchFilterProvider = intermediateProvider.GetRequiredService(); + var routingConvention = intermediateProvider.GetRequiredService(); + + _mvcBuilder.AddMvcOptions(options => + { + options.EnableEndpointRouting = true; + options.Filters.Add(exceptionFilterProvider.Get()); + options.Filters.Add(typeMatchFilterProvider.Get()); + options.InputFormatters.Insert(0, new JsonApiInputFormatter()); + options.OutputFormatters.Insert(0, new JsonApiOutputFormatter()); + options.Conventions.Insert(0, routingConvention); + }); + _services.AddSingleton(routingConvention); + } + + /// + /// Executes autodiscovery of JADNC services. + /// + public void AutoDiscover(Action autoDiscover) + { + autoDiscover(_serviceDiscoveryFacade); + } + + /// + /// Executes the action provided by the user to configure the resources using + /// + /// + public void ConfigureResources(Action resourceGraphBuilder) + { + resourceGraphBuilder(_resourceGraphBuilder); + } + + /// + /// Executes the action provided by the user to configure the resources using . + /// Additionally, inspects the EF core database context for models that implement IIdentifiable. + /// + public void ConfigureResources(Action resourceGraphBuilder) where TContext : DbContext + { + _resourceGraphBuilder.AddDbContext(); + _usesDbContext = true; + _services.AddScoped>(); + resourceGraphBuilder?.Invoke(_resourceGraphBuilder); + } + + /// + /// Registers the remaining internals. + /// + public void ConfigureServices() + { + var resourceGraph = _resourceGraphBuilder.Build(); + + if (!_usesDbContext) + { + _services.AddScoped(); + _services.AddSingleton(new DbContextOptionsBuilder().Options); + } + + _services.AddScoped(typeof(IResourceRepository<>), typeof(DefaultResourceRepository<>)); + _services.AddScoped(typeof(IResourceRepository<,>), typeof(DefaultResourceRepository<,>)); + + _services.AddScoped(typeof(IResourceReadRepository<,>), typeof(DefaultResourceRepository<,>)); + _services.AddScoped(typeof(IResourceWriteRepository<,>), typeof(DefaultResourceRepository<,>)); + + _services.AddScoped(typeof(ICreateService<>), typeof(DefaultResourceService<>)); + _services.AddScoped(typeof(ICreateService<,>), typeof(DefaultResourceService<,>)); + + _services.AddScoped(typeof(IGetAllService<>), typeof(DefaultResourceService<>)); + _services.AddScoped(typeof(IGetAllService<,>), typeof(DefaultResourceService<,>)); + + _services.AddScoped(typeof(IGetByIdService<>), typeof(DefaultResourceService<>)); + _services.AddScoped(typeof(IGetByIdService<,>), typeof(DefaultResourceService<,>)); + + _services.AddScoped(typeof(IGetRelationshipService<,>), typeof(DefaultResourceService<>)); + _services.AddScoped(typeof(IGetRelationshipService<,>), typeof(DefaultResourceService<,>)); + + _services.AddScoped(typeof(IUpdateService<>), typeof(DefaultResourceService<>)); + _services.AddScoped(typeof(IUpdateService<,>), typeof(DefaultResourceService<,>)); + + _services.AddScoped(typeof(IDeleteService<>), typeof(DefaultResourceService<>)); + _services.AddScoped(typeof(IDeleteService<,>), typeof(DefaultResourceService<,>)); + + _services.AddScoped(typeof(IResourceService<>), typeof(DefaultResourceService<>)); + _services.AddScoped(typeof(IResourceService<,>), typeof(DefaultResourceService<,>)); + + _services.AddSingleton(JsonApiOptions); + _services.AddSingleton(resourceGraph); + _services.AddSingleton(); + _services.AddSingleton(resourceGraph); + _services.AddSingleton(resourceGraph); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(typeof(RepositoryRelationshipUpdateHelper<>)); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + + AddServerSerialization(); + AddQueryParameterServices(); + if (JsonApiOptions.EnableResourceHooks) + AddResourceHooks(); + + _services.AddScoped(); + } + + private void AddQueryParameterServices() + { + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + } + + private void AddResourceHooks() + { + _services.AddSingleton(typeof(IHooksDiscovery<>), typeof(HooksDiscovery<>)); + _services.AddScoped(typeof(IResourceHookContainer<>), typeof(ResourceDefinition<>)); + _services.AddTransient(typeof(IResourceHookExecutor), typeof(ResourceHookExecutor)); + _services.AddTransient(); + _services.AddTransient(); + } + + private void AddServerSerialization() + { + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(typeof(IMetaBuilder<>), typeof(MetaBuilder<>)); + _services.AddScoped(typeof(ResponseSerializer<>)); + _services.AddScoped(sp => sp.GetRequiredService().GetSerializer()); + _services.AddScoped(); + } + + private void RegisterJsonApiStartupServices() + { + _services.AddSingleton(JsonApiOptions); + _services.TryAddSingleton(new KebabCaseFormatter()); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(sp => new ServiceDiscoveryFacade(_services, sp.GetRequiredService())); + _services.TryAddScoped(); + _services.TryAddScoped(); + } + } +} diff --git a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs deleted file mode 100644 index 3de45558c4..0000000000 --- a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using JsonApiDotNetCore.Services; -using Microsoft.AspNetCore.Http; - -namespace JsonApiDotNetCore.Builders -{ - public class LinkBuilder - { - private readonly IJsonApiContext _context; - - public LinkBuilder(IJsonApiContext context) - { - _context = context; - } - - public string GetBasePath(HttpContext context, string entityName) - { - var r = context.Request; - return (_context.Options.RelativeLinks) - ? GetNamespaceFromPath(r.Path, entityName) - : $"{r.Scheme}://{r.Host}{GetNamespaceFromPath(r.Path, entityName)}"; - } - - internal static string GetNamespaceFromPath(string path, string entityName) - { - var entityNameSpan = entityName.AsSpan(); - var pathSpan = path.AsSpan(); - const char delimiter = '/'; - for (var i = 0; i < pathSpan.Length; i++) - { - if(pathSpan[i].Equals(delimiter)) - { - var nextPosition = i + 1; - if(pathSpan.Length > i + entityNameSpan.Length) - { - var possiblePathSegment = pathSpan.Slice(nextPosition, entityNameSpan.Length); - if (entityNameSpan.SequenceEqual(possiblePathSegment)) - { - // check to see if it's the last position in the string - // or if the next character is a / - var lastCharacterPosition = nextPosition + entityNameSpan.Length; - - if(lastCharacterPosition == pathSpan.Length || pathSpan.Length >= lastCharacterPosition + 2 && pathSpan[lastCharacterPosition].Equals(delimiter)) - { - return pathSpan.Slice(0, i).ToString(); - } - } - } - } - } - - return string.Empty; - } - - public string GetSelfRelationLink(string parent, string parentId, string child) - { - return $"{_context.BasePath}/{parent}/{parentId}/relationships/{child}"; - } - - public string GetRelatedRelationLink(string parent, string parentId, string child) - { - return $"{_context.BasePath}/{parent}/{parentId}/{child}"; - } - - public string GetPageLink(int pageOffset, int pageSize) - { - var filterQueryComposer = new QueryComposer(); - var filters = filterQueryComposer.Compose(_context); - return $"{_context.BasePath}/{_context.RequestEntity.EntityName}?page[size]={pageSize}&page[number]={pageOffset}{filters}"; - } - } -} diff --git a/src/JsonApiDotNetCore/Builders/MetaBuilder.cs b/src/JsonApiDotNetCore/Builders/MetaBuilder.cs deleted file mode 100644 index 14b80321f6..0000000000 --- a/src/JsonApiDotNetCore/Builders/MetaBuilder.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace JsonApiDotNetCore.Builders -{ - public class MetaBuilder : IMetaBuilder - { - private Dictionary _meta = new Dictionary(); - - public void Add(string key, object value) - { - _meta[key] = value; - } - - /// - /// Joins the new dictionary with the current one. In the event of a key collision, - /// the new value will override the old. - /// - public void Add(Dictionary values) - { - _meta = values.Keys.Union(_meta.Keys) - .ToDictionary(key => key, - key => values.ContainsKey(key) ? values[key] : _meta[key]); - } - - public Dictionary Build() - { - return _meta; - } - } -} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs similarity index 63% rename from src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs rename to src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs index dfa6b665a0..405fe64936 100644 --- a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs @@ -7,91 +7,44 @@ using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.Links; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Builders { - public interface IResourceGraphBuilder - { - /// - /// Construct the - /// - IResourceGraph Build(); - - /// - /// Add a json:api resource - /// - /// The resource model type - /// - /// The pluralized name that should be exposed by the API. - /// If nothing is specified, the configured name formatter will be used. - /// See . - /// - IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable; - - /// - /// Add a json:api resource - /// - /// The resource model type - /// The resource model identifier type - /// - /// The pluralized name that should be exposed by the API. - /// If nothing is specified, the configured name formatter will be used. - /// See . - /// - IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable; - - /// - /// Add a json:api resource - /// - /// The resource model type - /// The resource model identifier type - /// - /// The pluralized name that should be exposed by the API. - /// If nothing is specified, the configured name formatter will be used. - /// See . - /// - IResourceGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName = null); - - /// - /// Add all the models that are part of the provided - /// that also implement - /// - /// The implementation type. - IResourceGraphBuilder AddDbContext() where T : DbContext; - - /// - /// Specify the used to format resource names. - /// - /// Formatter used to define exposed resource names by convention. - IResourceGraphBuilder UseNameFormatter(IResourceNameFormatter resourceNameFormatter); - - /// - /// Which links to include. Defaults to . - /// - Link DocumentLinks { get; set; } - } - public class ResourceGraphBuilder : IResourceGraphBuilder { - private List _entities = new List(); - private List _validationResults = new List(); - private bool _usesDbContext; - private IResourceNameFormatter _resourceNameFormatter = JsonApiOptions.ResourceNameFormatter; + private readonly List _entities = new List(); + private readonly List _validationResults = new List(); + private readonly IResourceNameFormatter _resourceNameFormatter = new KebabCaseFormatter(); - /// - public Link DocumentLinks { get; set; } = Link.All; + public ResourceGraphBuilder() { } + + public ResourceGraphBuilder(IResourceNameFormatter formatter) + { + _resourceNameFormatter = formatter; + } /// public IResourceGraph Build() { - // this must be done at build so that call order doesn't matter - _entities.ForEach(e => e.Links = GetLinkFlags(e.EntityType)); + _entities.ForEach(SetResourceLinksOptions); + var resourceGraph = new ResourceGraph(_entities, _validationResults); + return resourceGraph; + } - var graph = new ResourceGraph(_entities, _usesDbContext, _validationResults); - return graph; + private void SetResourceLinksOptions(ResourceContext resourceContext) + { + var attribute = (LinksAttribute)resourceContext.ResourceType.GetCustomAttribute(typeof(LinksAttribute)); + if (attribute != null) + { + resourceContext.RelationshipLinks = attribute.RelationshipLinks; + resourceContext.ResourceLinks = attribute.ResourceLinks; + resourceContext.TopLevelLinks = attribute.TopLevelLinks; + } } /// @@ -114,24 +67,16 @@ public IResourceGraphBuilder AddResource(Type entityType, Type idType, string pl return this; } - private ContextEntity GetEntity(string pluralizedTypeName, Type entityType, Type idType) => new ContextEntity + private ResourceContext GetEntity(string pluralizedTypeName, Type entityType, Type idType) => new ResourceContext { - EntityName = pluralizedTypeName, - EntityType = entityType, + ResourceName = pluralizedTypeName, + ResourceType = entityType, IdentityType = idType, Attributes = GetAttributes(entityType), Relationships = GetRelationships(entityType), - ResourceType = GetResourceDefinitionType(entityType) + ResourceDefinitionType = GetResourceDefinitionType(entityType) }; - private Link GetLinkFlags(Type entityType) - { - var attribute = (LinksAttribute)entityType.GetTypeInfo().GetCustomAttribute(typeof(LinksAttribute)); - if (attribute != null) - return attribute.Links; - - return DocumentLinks; - } protected virtual List GetAttributes(Type entityType) { @@ -141,11 +86,14 @@ protected virtual List GetAttributes(Type entityType) foreach (var prop in properties) { + /// todo: investigate why this is added in the exposed attributes list + /// because it is not really defined attribute considered from the json:api + /// spec point of view. if (prop.Name == nameof(Identifiable.Id)) { var idAttr = new AttrAttribute() { - PublicAttributeName = JsonApiOptions.ResourceNameFormatter.FormatPropertyName(prop), + PublicAttributeName = _resourceNameFormatter.FormatPropertyName(prop), PropertyInfo = prop, InternalAttributeName = prop.Name }; @@ -157,7 +105,7 @@ protected virtual List GetAttributes(Type entityType) if (attribute == null) continue; - attribute.PublicAttributeName = attribute.PublicAttributeName ?? JsonApiOptions.ResourceNameFormatter.FormatPropertyName(prop); + attribute.PublicAttributeName = attribute.PublicAttributeName ?? _resourceNameFormatter.FormatPropertyName(prop); attribute.InternalAttributeName = prop.Name; attribute.PropertyInfo = prop; @@ -169,30 +117,29 @@ protected virtual List GetAttributes(Type entityType) protected virtual List GetRelationships(Type entityType) { var attributes = new List(); - var properties = entityType.GetProperties(); - foreach (var prop in properties) { var attribute = (RelationshipAttribute)prop.GetCustomAttribute(typeof(RelationshipAttribute)); if (attribute == null) continue; - attribute.PublicRelationshipName = attribute.PublicRelationshipName ?? JsonApiOptions.ResourceNameFormatter.FormatPropertyName(prop); + attribute.PublicRelationshipName = attribute.PublicRelationshipName ?? _resourceNameFormatter.FormatPropertyName(prop); attribute.InternalRelationshipName = prop.Name; - attribute.DependentType = GetRelationshipType(attribute, prop); - attribute.PrincipalType = entityType; + attribute.RightType = GetRelationshipType(attribute, prop); + attribute.LeftType = entityType; attributes.Add(attribute); - if (attribute is HasManyThroughAttribute hasManyThroughAttribute) { + if (attribute is HasManyThroughAttribute hasManyThroughAttribute) + { var throughProperty = properties.SingleOrDefault(p => p.Name == hasManyThroughAttribute.InternalThroughName); - if(throughProperty == null) + if (throughProperty == null) throw new JsonApiSetupException($"Invalid '{nameof(HasManyThroughAttribute)}' on type '{entityType}'. Type does not contain a property named '{hasManyThroughAttribute.InternalThroughName}'."); - - if(throughProperty.PropertyType.Implements() == false) + + if (throughProperty.PropertyType.Implements() == false) throw new JsonApiSetupException($"Invalid '{nameof(HasManyThroughAttribute)}' on type '{entityType}.{throughProperty.Name}'. Property type does not implement IList."); - + // assumption: the property should be a generic collection, e.g. List - if(throughProperty.PropertyType.IsGenericType == false) + if (throughProperty.PropertyType.IsGenericType == false) throw new JsonApiSetupException($"Invalid '{nameof(HasManyThroughAttribute)}' on type '{entityType}'. Expected through entity to be a generic type, such as List<{prop.PropertyType}>."); // Article → List @@ -202,7 +149,7 @@ protected virtual List GetRelationships(Type entityType) hasManyThroughAttribute.ThroughType = throughProperty.PropertyType.GetGenericArguments()[0]; var throughProperties = hasManyThroughAttribute.ThroughType.GetProperties(); - + // ArticleTag.Article hasManyThroughAttribute.LeftProperty = throughProperties.SingleOrDefault(x => x.PropertyType == entityType) ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {entityType}"); @@ -213,13 +160,13 @@ protected virtual List GetRelationships(Type entityType) ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {entityType} with name {leftIdPropertyName}"); // Article → ArticleTag.Tag - hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.DependentType) - ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {hasManyThroughAttribute.DependentType}"); - + hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.RightType) + ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {hasManyThroughAttribute.RightType}"); + // ArticleTag.TagId var rightIdPropertyName = JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(hasManyThroughAttribute.RightProperty.Name); hasManyThroughAttribute.RightIdProperty = throughProperties.SingleOrDefault(x => x.Name == rightIdPropertyName) - ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {hasManyThroughAttribute.DependentType} with name {rightIdPropertyName}"); + ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a relationship id property to type {hasManyThroughAttribute.RightType} with name {rightIdPropertyName}"); } } @@ -234,25 +181,17 @@ protected virtual Type GetRelationshipType(RelationshipAttribute relation, Prope /// public IResourceGraphBuilder AddDbContext() where T : DbContext { - _usesDbContext = true; - var contextType = typeof(T); - var contextProperties = contextType.GetProperties(); - foreach (var property in contextProperties) { var dbSetType = property.PropertyType; - if (dbSetType.GetTypeInfo().IsGenericType && dbSetType.GetGenericTypeDefinition() == typeof(DbSet<>)) { var entityType = dbSetType.GetGenericArguments()[0]; - AssertEntityIsNotAlreadyDefined(entityType); - var (isJsonApiResource, idType) = GetIdType(entityType); - if (isJsonApiResource) _entities.Add(GetEntity(GetResourceNameFromDbSetProperty(property, entityType), entityType, idType)); } @@ -295,15 +234,8 @@ private string GetResourceNameFromDbSetProperty(PropertyInfo property, Type reso private void AssertEntityIsNotAlreadyDefined(Type entityType) { - if (_entities.Any(e => e.EntityType == entityType)) - throw new InvalidOperationException($"Cannot add entity type {entityType} to context graph, there is already an entity of that type configured."); - } - - /// - public IResourceGraphBuilder UseNameFormatter(IResourceNameFormatter resourceNameFormatter) - { - _resourceNameFormatter = resourceNameFormatter; - return this; + if (_entities.Any(e => e.ResourceType == entityType)) + throw new InvalidOperationException($"Cannot add entity type {entityType} to context resourceGraph, there is already an entity of that type configured."); } } } diff --git a/src/JsonApiDotNetCore/Configuration/DefaultAttributeResponseBehavior.cs b/src/JsonApiDotNetCore/Configuration/DefaultAttributeResponseBehavior.cs new file mode 100644 index 0000000000..48801eed01 --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/DefaultAttributeResponseBehavior.cs @@ -0,0 +1,35 @@ +namespace JsonApiDotNetCore.Configuration +{ + /// + /// Allows default valued attributes to be ommitted from the response payload + /// + public struct DefaultAttributeResponseBehavior + { + + /// Do not serialize default value attributes + /// + /// Allow clients to override the serialization behavior through a query parmeter. + /// + /// ``` + /// GET /articles?omitDefaultValuedAttributes=true + /// ``` + /// + /// + public DefaultAttributeResponseBehavior(bool omitNullValuedAttributes = false, bool allowClientOverride = false) + { + OmitDefaultValuedAttributes = omitNullValuedAttributes; + AllowClientOverride = allowClientOverride; + } + + /// + /// Do (not) include default valued attributes in the response payload. + /// + public bool OmitDefaultValuedAttributes { get; } + + /// + /// Allows clients to specify a `omitDefaultValuedAttributes` boolean query param to control + /// serialization behavior. + /// + public bool AllowClientOverride { get; } + } +} diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs new file mode 100644 index 0000000000..a969a4dbf0 --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -0,0 +1,33 @@ +namespace JsonApiDotNetCore.Configuration +{ + public interface IJsonApiOptions : ILinksConfiguration, ISerializerOptions + { + /// + /// Whether or not database values should be included by default + /// for resource hooks. Ignored if EnableResourceHooks is set false. + /// + /// Defaults to . + /// + bool LoaDatabaseValues { get; set; } + /// + /// Whether or not the total-record count should be included in all document + /// level meta objects. + /// Defaults to false. + /// + /// + /// options.IncludeTotalRecordCount = true; + /// + bool IncludeTotalRecordCount { get; set; } + int DefaultPageSize { get; } + bool ValidateModelState { get; } + bool AllowClientGeneratedIds { get; } + bool AllowCustomQueryParameters { get; set; } + string Namespace { get; set; } + } + + public interface ISerializerOptions + { + NullAttributeResponseBehavior NullAttributeResponseBehavior { get; set; } + DefaultAttributeResponseBehavior DefaultAttributeResponseBehavior { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Configuration/ILinksConfiguration.cs b/src/JsonApiDotNetCore/Configuration/ILinksConfiguration.cs new file mode 100644 index 0000000000..a372d61e67 --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/ILinksConfiguration.cs @@ -0,0 +1,71 @@ +using JsonApiDotNetCore.Models.Links; + +namespace JsonApiDotNetCore.Configuration +{ + /// + /// Options to configure links at a global level. + /// + public interface ILinksConfiguration + { + /// + /// Use relative links for all resources. + /// + /// + /// + /// options.RelativeLinks = true; + /// + /// + /// { + /// "type": "articles", + /// "id": "4309", + /// "relationships": { + /// "author": { + /// "links": { + /// "self": "/api/v1/articles/4309/relationships/author", + /// "related": "/api/v1/articles/4309/author" + /// } + /// } + /// } + /// } + /// + /// + bool RelativeLinks { get; } + /// + /// Configures globally which links to show in the + /// object for a requested resource. Setting can be overriden per resource by + /// adding a to the class definitio of that resource. + /// + Link TopLevelLinks { get; } + + /// + /// Configures globally which links to show in the + /// object for a requested resource. Setting can be overriden per resource by + /// adding a to the class definitio of that resource. + /// + Link ResourceLinks { get; } + /// + /// Configures globally which links to show in the + /// object for a requested resource. Setting can be overriden per resource by + /// adding a to the class definitio of that resource. + /// Option can also be specified per relationship by using the associated links argument + /// in the constructor of . + /// + /// + /// + /// options.DefaultRelationshipLinks = Link.None; + /// + /// + /// { + /// "type": "articles", + /// "id": "4309", + /// "relationships": { + /// "author": { "data": { "type": "people", "id": "1234" } + /// } + /// } + /// } + /// + /// + Link RelationshipLinks { get; } + + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 21efdb97ed..fe41af6602 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -1,26 +1,28 @@ -using System; using System.Collections.Generic; -using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Graph; -using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization; -using Microsoft.EntityFrameworkCore; +using JsonApiDotNetCore.Models.Links; using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Configuration { /// /// Global options /// - public class JsonApiOptions + public class JsonApiOptions : IJsonApiOptions { - /// - /// Provides an interface for formatting resource names by convention - /// - public static IResourceNameFormatter ResourceNameFormatter { get; set; } = new DefaultResourceNameFormatter(); + /// + public bool RelativeLinks { get; set; } = false; + + /// + public Link TopLevelLinks { get; set; } = Link.All; + + /// + public Link ResourceLinks { get; set; } = Link.All; + + /// + public Link RelationshipLinks { get; set; } = Link.All; /// /// Provides an interface for formatting relationship id properties given the navigation property name @@ -31,7 +33,7 @@ public class JsonApiOptions /// Whether or not stack traces should be serialized in Error objects /// public static bool DisableErrorStackTraces { get; set; } - + /// /// Whether or not source URLs should be serialized in Error objects /// @@ -50,7 +52,7 @@ public class JsonApiOptions /// /// Defaults to . /// - public bool LoadDatabaseValues { get; set; } = false; + public bool LoaDatabaseValues { get; set; } = false; /// /// The base URL Namespace @@ -89,55 +91,6 @@ public class JsonApiOptions /// public bool AllowClientGeneratedIds { get; set; } - /// - /// The graph of all resources exposed by this application. - /// - public IResourceGraph ResourceGraph { get; set; } - - /// - /// Use relative links for all resources. - /// - /// - /// - /// options.RelativeLinks = true; - /// - /// - /// { - /// "type": "articles", - /// "id": "4309", - /// "relationships": { - /// "author": { - /// "links": { - /// "self": "/api/v1/articles/4309/relationships/author", - /// "related": "/api/v1/articles/4309/author" - /// } - /// } - /// } - /// } - /// - /// - public bool RelativeLinks { get; set; } - - /// - /// Which links to include in relationships. Defaults to . - /// - /// - /// - /// options.DefaultRelationshipLinks = Link.None; - /// - /// - /// { - /// "type": "articles", - /// "id": "4309", - /// "relationships": { - /// "author": {} - /// } - /// } - /// } - /// - /// - public Link DefaultRelationshipLinks { get; set; } = Link.All; - /// /// Whether or not to allow all custom query parameters. /// @@ -159,17 +112,7 @@ public class JsonApiOptions /// /// public NullAttributeResponseBehavior NullAttributeResponseBehavior { get; set; } - - /// - /// Whether or not to allow json:api v1.1 operation requests. - /// This is a beta feature and there may be breaking changes - /// in subsequent releases. For now, it should be considered - /// experimental. - /// - /// - /// This will be enabled by default in a subsequent patch JsonApiDotNetCore v2.2.x - /// - public bool EnableOperations { get; set; } + public DefaultAttributeResponseBehavior DefaultAttributeResponseBehavior { get; set; } /// /// Whether or not to validate model state. @@ -183,32 +126,12 @@ public class JsonApiOptions public JsonSerializerSettings SerializerSettings { get; } = new JsonSerializerSettings() { - NullValueHandling = NullValueHandling.Ignore, - ContractResolver = new DasherizedResolver() + NullValueHandling = NullValueHandling.Ignore }; - public void BuildResourceGraph(Action builder) where TContext : DbContext - { - BuildResourceGraph(builder); - - ResourceGraphBuilder.AddDbContext(); - - ResourceGraph = ResourceGraphBuilder.Build(); - } - - public void BuildResourceGraph(Action builder) - { - if (builder == null) return; - - builder(ResourceGraphBuilder); - - ResourceGraph = ResourceGraphBuilder.Build(); - } - public void EnableExtension(JsonApiExtension extension) => EnabledExtensions.Add(extension); - internal IResourceGraphBuilder ResourceGraphBuilder { get; } = new ResourceGraphBuilder(); internal List EnabledExtensions { get; set; } = new List(); } } diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index 93fd4826e2..e0b781b283 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -1,41 +1,16 @@ -using System.Collections.Generic; +using System; +using System.Reflection; using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Controllers { - public class BaseJsonApiController - : BaseJsonApiController - where T : class, IIdentifiable - { - public BaseJsonApiController( - IJsonApiContext jsonApiContext, - IResourceService resourceService - ) : base(jsonApiContext, resourceService) { } - - public BaseJsonApiController( - IJsonApiContext jsonApiContext, - IResourceQueryService queryService = null, - IResourceCmdService cmdService = null - ) : base(jsonApiContext, queryService, cmdService) { } - - public BaseJsonApiController( - IJsonApiContext jsonApiContext, - IGetAllService getAll = null, - IGetByIdService getById = null, - IGetRelationshipService getRelationship = null, - IGetRelationshipsService getRelationships = null, - ICreateService create = null, - IUpdateService update = null, - IUpdateRelationshipService updateRelationships = null, - IDeleteService delete = null - ) : base(jsonApiContext, getAll, getById, getRelationship, getRelationships, create, update, updateRelationships, delete) { } - } - public class BaseJsonApiController : JsonApiControllerMixin where T : class, IIdentifiable @@ -48,13 +23,20 @@ public class BaseJsonApiController private readonly IUpdateService _update; private readonly IUpdateRelationshipService _updateRelationships; private readonly IDeleteService _delete; - private readonly IJsonApiContext _jsonApiContext; - + private readonly ILogger> _logger; + private readonly IJsonApiOptions _jsonApiOptions; + public BaseJsonApiController( - IJsonApiContext jsonApiContext, - IResourceService resourceService) + IJsonApiOptions jsonApiOptions, + IResourceService resourceService, + ILoggerFactory loggerFactory) { - _jsonApiContext = jsonApiContext.ApplyContext(this); + if (loggerFactory != null) + _logger = loggerFactory.CreateLogger>(); + else + _logger = new Logger>(new LoggerFactory()); + + _jsonApiOptions = jsonApiOptions; _getAll = resourceService; _getById = resourceService; _getRelationship = resourceService; @@ -66,11 +48,11 @@ public BaseJsonApiController( } public BaseJsonApiController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, IResourceQueryService queryService = null, IResourceCmdService cmdService = null) { - _jsonApiContext = jsonApiContext.ApplyContext(this); + _jsonApiOptions = jsonApiOptions; _getAll = queryService; _getById = queryService; _getRelationship = queryService; @@ -81,8 +63,17 @@ public BaseJsonApiController( _delete = cmdService; } + /// + /// + /// + /// + /// + /// + /// + /// + /// public BaseJsonApiController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, IGetAllService getAll = null, IGetByIdService getById = null, IGetRelationshipService getRelationship = null, @@ -92,7 +83,7 @@ public BaseJsonApiController( IUpdateRelationshipService updateRelationships = null, IDeleteService delete = null) { - _jsonApiContext = jsonApiContext.ApplyContext(this); + _jsonApiOptions = jsonApiOptions; _getAll = getAll; _getById = getById; _getRelationship = getRelationship; @@ -106,18 +97,14 @@ public BaseJsonApiController( public virtual async Task GetAsync() { if (_getAll == null) throw Exceptions.UnSupportedRequestMethod; - var entities = await _getAll.GetAsync(); - return Ok(entities); } public virtual async Task GetAsync(TId id) { if (_getById == null) throw Exceptions.UnSupportedRequestMethod; - var entity = await _getById.GetAsync(id); - if (entity == null) return NotFound(); @@ -126,8 +113,8 @@ public virtual async Task GetAsync(TId id) public virtual async Task GetRelationshipsAsync(TId id, string relationshipName) { - if (_getRelationships == null) throw Exceptions.UnSupportedRequestMethod; - + if (_getRelationships == null) + throw Exceptions.UnSupportedRequestMethod; var relationship = await _getRelationships.GetRelationshipsAsync(id, relationshipName); if (relationship == null) return NotFound(); @@ -138,9 +125,7 @@ public virtual async Task GetRelationshipsAsync(TId id, string re public virtual async Task GetRelationshipAsync(TId id, string relationshipName) { if (_getRelationship == null) throw Exceptions.UnSupportedRequestMethod; - var relationship = await _getRelationship.GetRelationshipAsync(id, relationshipName); - return Ok(relationship); } @@ -152,11 +137,11 @@ public virtual async Task PostAsync([FromBody] T entity) if (entity == null) return UnprocessableEntity(); - if (!_jsonApiContext.Options.AllowClientGeneratedIds && !string.IsNullOrEmpty(entity.StringId)) + if (!_jsonApiOptions.AllowClientGeneratedIds && !string.IsNullOrEmpty(entity.StringId)) return Forbidden(); - if (_jsonApiContext.Options.ValidateModelState && !ModelState.IsValid) - return UnprocessableEntity(ModelState.ConvertToErrorCollection(_jsonApiContext.ResourceGraph)); + if (_jsonApiOptions.ValidateModelState && !ModelState.IsValid) + return UnprocessableEntity(ModelState.ConvertToErrorCollection(GetAssociatedResource())); entity = await _create.CreateAsync(entity); @@ -166,12 +151,11 @@ public virtual async Task PostAsync([FromBody] T entity) public virtual async Task PatchAsync(TId id, [FromBody] T entity) { if (_update == null) throw Exceptions.UnSupportedRequestMethod; - if (entity == null) return UnprocessableEntity(); - if (_jsonApiContext.Options.ValidateModelState && !ModelState.IsValid) - return UnprocessableEntity(ModelState.ConvertToErrorCollection(_jsonApiContext.ResourceGraph)); + if (_jsonApiOptions.ValidateModelState && !ModelState.IsValid) + return UnprocessableEntity(ModelState.ConvertToErrorCollection(GetAssociatedResource())); var updatedEntity = await _update.UpdateAsync(id, entity); @@ -181,25 +165,55 @@ public virtual async Task PatchAsync(TId id, [FromBody] T entity) return Ok(updatedEntity); } - public virtual async Task PatchRelationshipsAsync(TId id, string relationshipName, [FromBody] List relationships) + public virtual async Task PatchRelationshipsAsync(TId id, string relationshipName, [FromBody] object relationships) { if (_updateRelationships == null) throw Exceptions.UnSupportedRequestMethod; - await _updateRelationships.UpdateRelationshipsAsync(id, relationshipName, relationships); - return Ok(); } public virtual async Task DeleteAsync(TId id) { if (_delete == null) throw Exceptions.UnSupportedRequestMethod; - var wasDeleted = await _delete.DeleteAsync(id); - if (!wasDeleted) return NotFound(); - return NoContent(); } + + internal Type GetAssociatedResource() + { + return GetType().GetMethod(nameof(GetAssociatedResource), BindingFlags.Instance | BindingFlags.NonPublic) + .DeclaringType + .GetGenericArguments()[0]; + } + } + public class BaseJsonApiController + : BaseJsonApiController + where T : class, IIdentifiable + { + public BaseJsonApiController( + IJsonApiOptions jsonApiOptions, + IResourceService resourceService + ) : base(jsonApiOptions, resourceService, resourceService) { } + + public BaseJsonApiController( + IJsonApiOptions jsonApiOptions, + IResourceQueryService queryService = null, + IResourceCmdService cmdService = null + ) : base(jsonApiOptions, queryService, cmdService) { } + + + public BaseJsonApiController( + IJsonApiOptions jsonApiOptions, + IGetAllService getAll = null, + IGetByIdService getById = null, + IGetRelationshipService getRelationship = null, + IGetRelationshipsService getRelationships = null, + ICreateService create = null, + IUpdateService update = null, + IUpdateRelationshipService updateRelationships = null, + IDeleteService delete = null + ) : base(jsonApiOptions, getAll, getById, getRelationship, getRelationships, create, update, updateRelationships, delete) { } } } diff --git a/src/JsonApiDotNetCore/Controllers/DisableQueryAttribute.cs b/src/JsonApiDotNetCore/Controllers/DisableQueryAttribute.cs index 40ebf385fe..d28bd06faf 100644 --- a/src/JsonApiDotNetCore/Controllers/DisableQueryAttribute.cs +++ b/src/JsonApiDotNetCore/Controllers/DisableQueryAttribute.cs @@ -3,12 +3,27 @@ namespace JsonApiDotNetCore.Controllers { public class DisableQueryAttribute : Attribute - { + { + /// + /// Disabled one of the native query parameters for a controller. + /// + /// public DisableQueryAttribute(QueryParams queryParams) { - QueryParams = queryParams; + QueryParams = queryParams.ToString("G").ToLower(); } - public QueryParams QueryParams { get; set; } + /// + /// It is allowed to use strings to indicate which query parameters + /// should be disabled, because the user may have defined a custom + /// query parameter that is not included in the enum. + /// + /// + public DisableQueryAttribute(string customQueryParams) + { + QueryParams = customQueryParams.ToLower(); + } + + public string QueryParams { get; } } } \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs index 16ab4aa74a..d88ee0272d 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs @@ -1,18 +1,18 @@ -using System.Collections.Generic; using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc; namespace JsonApiDotNetCore.Controllers { - public class JsonApiCmdController - : JsonApiCmdController where T : class, IIdentifiable + public class JsonApiCmdController : JsonApiCmdController + where T : class, IIdentifiable { public JsonApiCmdController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, IResourceService resourceService) - : base(jsonApiContext, resourceService) + : base(jsonApiOptions, resourceService) { } } @@ -20,9 +20,9 @@ public class JsonApiCmdController : BaseJsonApiController where T : class, IIdentifiable { public JsonApiCmdController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, IResourceService resourceService) - : base(jsonApiContext, resourceService) + : base(jsonApiOptions, resourceService) { } [HttpPost] @@ -35,7 +35,7 @@ public override async Task PatchAsync(TId id, [FromBody] T entity [HttpPatch("{id}/relationships/{relationshipName}")] public override async Task PatchRelationshipsAsync( - TId id, string relationshipName, [FromBody] List relationships) + TId id, string relationshipName, [FromBody] object relationships) => await base.PatchRelationshipsAsync(id, relationshipName, relationships); [HttpDelete("{id}")] diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs index a77c03da06..6ed4ccca04 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc; @@ -7,53 +7,21 @@ namespace JsonApiDotNetCore.Controllers { - public class JsonApiController - : JsonApiController where T : class, IIdentifiable + public class JsonApiController : BaseJsonApiController where T : class, IIdentifiable { - public JsonApiController( - IJsonApiContext jsonApiContext, - IResourceService resourceService, - ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) - { } - - public JsonApiController( - IJsonApiContext jsonApiContext, - IResourceService resourceService) - : base(jsonApiContext, resourceService) - { } - - public JsonApiController( - IJsonApiContext jsonApiContext, - IGetAllService getAll = null, - IGetByIdService getById = null, - IGetRelationshipService getRelationship = null, - IGetRelationshipsService getRelationships = null, - ICreateService create = null, - IUpdateService update = null, - IUpdateRelationshipService updateRelationships = null, - IDeleteService delete = null - ) : base(jsonApiContext, getAll, getById, getRelationship, getRelationships, create, update, updateRelationships, delete) { } - } - public class JsonApiController - : BaseJsonApiController where T : class, IIdentifiable - { + /// + /// + /// public JsonApiController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, IResourceService resourceService, - ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService) + ILoggerFactory loggerFactory = null) + : base(jsonApiOptions, resourceService, loggerFactory = null) { } public JsonApiController( - IJsonApiContext jsonApiContext, - IResourceService resourceService) - : base(jsonApiContext, resourceService) - { } - - public JsonApiController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, IGetAllService getAll = null, IGetByIdService getById = null, IGetRelationshipService getRelationship = null, @@ -62,7 +30,7 @@ public JsonApiController( IUpdateService update = null, IUpdateRelationshipService updateRelationships = null, IDeleteService delete = null - ) : base(jsonApiContext, getAll, getById, getRelationship, getRelationships, create, update, updateRelationships, delete) { } + ) : base(jsonApiOptions, getAll, getById, getRelationship, getRelationships, create, update, updateRelationships, delete) { } [HttpGet] public override async Task GetAsync() => await base.GetAsync(); @@ -84,14 +52,52 @@ public override async Task PostAsync([FromBody] T entity) [HttpPatch("{id}")] public override async Task PatchAsync(TId id, [FromBody] T entity) - => await base.PatchAsync(id, entity); + { + return await base.PatchAsync(id, entity); + } [HttpPatch("{id}/relationships/{relationshipName}")] public override async Task PatchRelationshipsAsync( - TId id, string relationshipName, [FromBody] List relationships) + TId id, string relationshipName, [FromBody] object relationships) => await base.PatchRelationshipsAsync(id, relationshipName, relationships); [HttpDelete("{id}")] public override async Task DeleteAsync(TId id) => await base.DeleteAsync(id); } + + /// + /// JsonApiController with int as default + /// + /// + public class JsonApiController : JsonApiController where T : class, IIdentifiable + { + /// + /// Base constructor with int as default + /// + /// + /// + /// + /// + public JsonApiController( + IJsonApiOptions jsonApiOptions, + IResourceService resourceService, + ILoggerFactory loggerFactory = null + ) + : base(jsonApiOptions, resourceService, loggerFactory) + { } + + public JsonApiController( + IJsonApiOptions jsonApiOptions, + IGetAllService getAll = null, + IGetByIdService getById = null, + IGetRelationshipService getRelationship = null, + IGetRelationshipsService getRelationships = null, + ICreateService create = null, + IUpdateService update = null, + IUpdateRelationshipService updateRelationships = null, + IDeleteService delete = null + ) : base(jsonApiOptions, getAll, getById, getRelationship, getRelationships, create, update, updateRelationships, delete) { } + + + } } diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs b/src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs index b80d912bed..0fa06cab27 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs @@ -1,10 +1,10 @@ -using System.Collections.Generic; -using System.Linq; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Middleware; using Microsoft.AspNetCore.Mvc; namespace JsonApiDotNetCore.Controllers { + [ServiceFilter(typeof(IQueryParameterActionFilter))] public abstract class JsonApiControllerMixin : ControllerBase { protected IActionResult Forbidden() diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiOperationsController.cs deleted file mode 100644 index f6db9f0d06..0000000000 --- a/src/JsonApiDotNetCore/Controllers/JsonApiOperationsController.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Threading.Tasks; -using JsonApiDotNetCore.Models.Operations; -using JsonApiDotNetCore.Services.Operations; -using Microsoft.AspNetCore.Mvc; - -namespace JsonApiDotNetCore.Controllers -{ - /// - /// A controller to be used for bulk operations as defined in the json:api 1.1 specification - /// - public class JsonApiOperationsController : ControllerBase - { - private readonly IOperationsProcessor _operationsProcessor; - - /// - /// The processor to handle bulk operations. - /// - public JsonApiOperationsController(IOperationsProcessor operationsProcessor) - { - _operationsProcessor = operationsProcessor; - } - - /// - /// Bulk endpoint for json:api operations - /// - /// - /// A json:api operations request document - /// - /// - /// - /// PATCH /api/bulk HTTP/1.1 - /// Content-Type: application/vnd.api+json - /// - /// { - /// "operations": [{ - /// "op": "add", - /// "ref": { - /// "type": "authors" - /// }, - /// "data": { - /// "type": "authors", - /// "attributes": { - /// "name": "jaredcnance" - /// } - /// } - /// }] - /// } - /// - /// - [HttpPatch] - public virtual async Task PatchAsync([FromBody] OperationsDocument doc) - { - if (doc == null) return new StatusCodeResult(422); - - var results = await _operationsProcessor.ProcessAsync(doc.Operations); - - return Ok(new OperationsDocument(results)); - } - } -} diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs index 5211e5fa3b..1d49b984ea 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc; @@ -9,20 +10,24 @@ public class JsonApiQueryController : JsonApiQueryController where T : class, IIdentifiable { public JsonApiQueryController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, IResourceService resourceService) - : base(jsonApiContext, resourceService) - { } + : base(jsonApiOptions, resourceService) { } } public class JsonApiQueryController : BaseJsonApiController where T : class, IIdentifiable { public JsonApiQueryController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiContext, + IResourceQueryService resourceQueryService) + : base(jsonApiContext, resourceQueryService) { } + + + public JsonApiQueryController( + IJsonApiOptions jsonApiOptions, IResourceService resourceService) - : base(jsonApiContext, resourceService) - { } + : base(jsonApiOptions, resourceService) { } [HttpGet] public override async Task GetAsync() => await base.GetAsync(); diff --git a/src/JsonApiDotNetCore/Controllers/QueryParams.cs b/src/JsonApiDotNetCore/Controllers/QueryParams.cs index 4c963098ad..6e5e3901fb 100644 --- a/src/JsonApiDotNetCore/Controllers/QueryParams.cs +++ b/src/JsonApiDotNetCore/Controllers/QueryParams.cs @@ -2,7 +2,7 @@ namespace JsonApiDotNetCore.Controllers { public enum QueryParams { - Filter = 1 << 0, + Filters = 1 << 0, Sort = 1 << 1, Include = 1 << 2, Page = 1 << 3, diff --git a/src/JsonApiDotNetCore/Data/Article.cs b/src/JsonApiDotNetCore/Data/Article.cs deleted file mode 100644 index 004d5d2f71..0000000000 --- a/src/JsonApiDotNetCore/Data/Article.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace JsonApiDotNetCore.Data -{ - -} diff --git a/src/JsonApiDotNetCore/Data/DbContextResolver.cs b/src/JsonApiDotNetCore/Data/DbContextResolver.cs index 7ce7eec921..3c9e20b9da 100644 --- a/src/JsonApiDotNetCore/Data/DbContextResolver.cs +++ b/src/JsonApiDotNetCore/Data/DbContextResolver.cs @@ -1,4 +1,3 @@ -using JsonApiDotNetCore.Extensions; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCore.Data @@ -15,7 +14,7 @@ public DbContextResolver(TContext context) public DbContext GetContext() => _context; - public DbSet GetDbSet() where TEntity : class => null; + public DbSet GetDbSet() where TResource : class => null; } } diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs deleted file mode 100644 index 889e2f7198..0000000000 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ /dev/null @@ -1,584 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Generics; -using JsonApiDotNetCore.Internal.Query; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -namespace JsonApiDotNetCore.Data -{ - /// - public class DefaultEntityRepository - : DefaultEntityRepository, - IEntityRepository - where TEntity : class, IIdentifiable - { - public DefaultEntityRepository( - IJsonApiContext jsonApiContext, - IDbContextResolver contextResolver, - ResourceDefinition resourceDefinition = null) - : base(jsonApiContext, contextResolver, resourceDefinition) - { } - - public DefaultEntityRepository( - ILoggerFactory loggerFactory, - IJsonApiContext jsonApiContext, - IDbContextResolver contextResolver, - ResourceDefinition resourceDefinition = null) - : base(loggerFactory, jsonApiContext, contextResolver, resourceDefinition) - { } - } - - /// - /// Provides a default repository implementation and is responsible for - /// abstracting any EF Core APIs away from the service layer. - /// - public class DefaultEntityRepository - : IEntityRepository, - IEntityFrameworkRepository - where TEntity : class, IIdentifiable - { - private readonly DbContext _context; - private readonly DbSet _dbSet; - private readonly ILogger _logger; - private readonly IJsonApiContext _jsonApiContext; - private readonly IGenericProcessorFactory _genericProcessorFactory; - private readonly ResourceDefinition _resourceDefinition; - public DefaultEntityRepository( - IJsonApiContext jsonApiContext, - IDbContextResolver contextResolver, - ResourceDefinition resourceDefinition = null) - { - _context = contextResolver.GetContext(); - _dbSet = _context.Set(); - _jsonApiContext = jsonApiContext; - _genericProcessorFactory = _jsonApiContext.GenericProcessorFactory; - _resourceDefinition = resourceDefinition; - } - - public DefaultEntityRepository( - ILoggerFactory loggerFactory, - IJsonApiContext jsonApiContext, - IDbContextResolver contextResolver, - ResourceDefinition resourceDefinition = null) - { - _context = contextResolver.GetContext(); - _dbSet = _context.Set(); - _jsonApiContext = jsonApiContext; - _logger = loggerFactory.CreateLogger>(); - _genericProcessorFactory = _jsonApiContext.GenericProcessorFactory; - _resourceDefinition = resourceDefinition; - } - - /// - public virtual IQueryable Get() - => _dbSet; - - /// - public virtual IQueryable Select(IQueryable entities, List fields) - { - if (fields?.Count > 0) - return entities.Select(fields); - - return entities; - } - - /// - public virtual IQueryable Filter(IQueryable entities, FilterQuery filterQuery) - { - if (_resourceDefinition != null) - { - var defaultQueryFilters = _resourceDefinition.GetQueryFilters(); - if (defaultQueryFilters != null && defaultQueryFilters.TryGetValue(filterQuery.Attribute, out var defaultQueryFilter) == true) - { - return defaultQueryFilter(entities, filterQuery); - } - } - return entities.Filter(_jsonApiContext, filterQuery); - } - - /// - public virtual IQueryable Sort(IQueryable entities, List sortQueries) - { - if (sortQueries != null && sortQueries.Count > 0) - return entities.Sort(_jsonApiContext, sortQueries); - - if (_resourceDefinition != null) - { - var defaultSortOrder = _resourceDefinition.DefaultSort(); - if (defaultSortOrder != null && defaultSortOrder.Count > 0) - { - foreach (var sortProp in defaultSortOrder) - { - // this is dumb...add an overload, don't allocate for no reason - entities.Sort(_jsonApiContext, new SortQuery(sortProp.Item2, sortProp.Item1.PublicAttributeName)); - } - } - } - return entities; - } - - /// - public virtual async Task GetAsync(TId id) - { - return await Select(Get(), _jsonApiContext.QuerySet?.Fields).SingleOrDefaultAsync(e => e.Id.Equals(id)); - } - - /// - public virtual async Task GetAndIncludeAsync(TId id, string relationshipName) - { - _logger?.LogDebug($"[JADN] GetAndIncludeAsync({id}, {relationshipName})"); - - var includedSet = Include(Select(Get(), _jsonApiContext.QuerySet?.Fields), relationshipName); - var result = await includedSet.SingleOrDefaultAsync(e => e.Id.Equals(id)); - - return result; - } - - /// - public virtual async Task CreateAsync(TEntity entity) - { - foreach (var relationshipAttr in _jsonApiContext.RelationshipsToUpdate?.Keys) - { - var trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, entity, out bool wasAlreadyTracked); - LoadInverseRelationships(trackedRelationshipValue, relationshipAttr); - if (wasAlreadyTracked) - { - /// We only need to reassign the relationship value to the to-be-added - /// entity when we're using a different instance (because this different one - /// was already tracked) than the one assigned to the to-be-created entity. - AssignRelationshipValue(entity, trackedRelationshipValue, relationshipAttr); - } - else if (relationshipAttr is HasManyThroughAttribute throughAttr) - { - /// even if we don't have to reassign anything because of already tracked - /// entities, we still need to assign the "through" entities in the case of many-to-many. - AssignHasManyThrough(entity, throughAttr, (IList)trackedRelationshipValue); - } - } - _dbSet.Add(entity); - await _context.SaveChangesAsync(); - - return entity; - } - - /// - /// Loads the inverse relationships to prevent foreign key constraints from being violated - /// to support implicit removes, see https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/502. - /// - /// Consider the following example: - /// person.todoItems = [t1,t2] is updated to [t3, t4]. If t3, and/or t4 was - /// already related to a other person, and these persons are NOT loaded in to the - /// db context, then the query may cause a foreign key constraint. Loading - /// these "inverse relationships" into the DB context ensures EF core to take - /// this into account. - /// - /// - private void LoadInverseRelationships(object trackedRelationshipValue, RelationshipAttribute relationshipAttr) - { - if (relationshipAttr.InverseNavigation == null || trackedRelationshipValue == null) return; - if (relationshipAttr is HasOneAttribute hasOneAttr) - { - var relationEntry = _context.Entry((IIdentifiable)trackedRelationshipValue); - if (IsHasOneRelationship(hasOneAttr.InverseNavigation, trackedRelationshipValue.GetType())) - { - relationEntry.Reference(hasOneAttr.InverseNavigation).Load(); - } - else - { - relationEntry.Collection(hasOneAttr.InverseNavigation).Load(); - } - } - else if (relationshipAttr is HasManyAttribute hasManyAttr && !(relationshipAttr is HasManyThroughAttribute)) - { - foreach (IIdentifiable relationshipValue in (IList)trackedRelationshipValue) - { - _context.Entry(relationshipValue).Reference(hasManyAttr.InverseNavigation).Load(); - } - } - } - - - private bool IsHasOneRelationship(string internalRelationshipName, Type type) - { - var relationshipAttr = _jsonApiContext.ResourceGraph.GetContextEntity(type).Relationships.SingleOrDefault(r => r.InternalRelationshipName == internalRelationshipName); - if(relationshipAttr != null) - { - if (relationshipAttr is HasOneAttribute) return true; - return false; - } else - { - // relationshipAttr is null when we don't put a [RelationshipAttribute] on the inverse navigation property. - // In this case we use relfection to figure out what kind of relationship is pointing back. - return !(type.GetProperty(internalRelationshipName).PropertyType.Inherits(typeof(IEnumerable))); - } - } - - - /// - public void DetachRelationshipPointers(TEntity entity) - { - - foreach (var relationshipAttr in _jsonApiContext.RelationshipsToUpdate.Keys) - { - if (relationshipAttr is HasOneAttribute hasOneAttr) - { - var relationshipValue = GetEntityResourceSeparationValue(entity, hasOneAttr) ?? (IIdentifiable)hasOneAttr.GetValue(entity); - if (relationshipValue == null) continue; - _context.Entry(relationshipValue).State = EntityState.Detached; - - } - else - { - IEnumerable relationshipValueList = (IEnumerable)relationshipAttr.GetValue(entity); - /// This adds support for resource-entity separation in the case of one-to-many. - /// todo: currently there is no support for many to many relations. - if (relationshipAttr is HasManyAttribute hasMany) - relationshipValueList = GetEntityResourceSeparationValue(entity, hasMany) ?? relationshipValueList; - if (relationshipValueList == null) continue; - foreach (var pointer in relationshipValueList) - { - _context.Entry(pointer).State = EntityState.Detached; - } - /// detaching has many relationships is not sufficient to - /// trigger a full reload of relationships: the navigation - /// property actually needs to be nulled out, otherwise - /// EF will still add duplicate instances to the collection - relationshipAttr.SetValue(entity, null); - } - } - } - - [Obsolete("Use overload UpdateAsync(TEntity updatedEntity): providing parameter ID does no longer add anything relevant")] - public virtual async Task UpdateAsync(TId id, TEntity updatedEntity) - { - return await UpdateAsync(updatedEntity); - } - - /// - public virtual async Task UpdateAsync(TEntity updatedEntity) - { - var databaseEntity = await GetAsync(updatedEntity.Id); - if (databaseEntity == null) - return null; - - foreach (var attr in _jsonApiContext.AttributesToUpdate.Keys) - attr.SetValue(databaseEntity, attr.GetValue(updatedEntity)); - - foreach (var relationshipAttr in _jsonApiContext.RelationshipsToUpdate?.Keys) - { - /// loads databasePerson.todoItems - LoadCurrentRelationships(databaseEntity, relationshipAttr); - /// trackedRelationshipValue is either equal to updatedPerson.todoItems - /// or replaced with the same set of todoItems from the EF Core change tracker, - /// if they were already tracked - object trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, updatedEntity, out bool wasAlreadyTracked); - /// loads into the db context any persons currently related - /// to the todoItems in trackedRelationshipValue - LoadInverseRelationships(trackedRelationshipValue, relationshipAttr); - /// assigns the updated relationship to the database entity - AssignRelationshipValue(databaseEntity, trackedRelationshipValue, relationshipAttr); - } - - await _context.SaveChangesAsync(); - return databaseEntity; - } - - - /// - /// Responsible for getting the relationship value for a given relationship - /// attribute of a given entity. It ensures that the relationship value - /// that it returns is attached to the database without reattaching duplicates instances - /// to the change tracker. It does so by checking if there already are - /// instances of the to-be-attached entities in the change tracker. - /// - private object GetTrackedRelationshipValue(RelationshipAttribute relationshipAttr, TEntity entity, out bool wasAlreadyAttached) - { - wasAlreadyAttached = false; - if (relationshipAttr is HasOneAttribute hasOneAttr) - { - /// This adds support for resource-entity separation in the case of one-to-one. - var relationshipValue = GetEntityResourceSeparationValue(entity, hasOneAttr) ?? (IIdentifiable)hasOneAttr.GetValue(entity); - if (relationshipValue == null) - return null; - return GetTrackedHasOneRelationshipValue(relationshipValue, hasOneAttr, ref wasAlreadyAttached); - } - else - { - IEnumerable relationshipValueList = (IEnumerable)relationshipAttr.GetValue(entity); - /// This adds support for resource-entity separation in the case of one-to-many. - /// todo: currently there is no support for many to many relations. - if (relationshipAttr is HasManyAttribute hasMany) - relationshipValueList = GetEntityResourceSeparationValue(entity, hasMany) ?? relationshipValueList; - if (relationshipValueList == null) return null; - return GetTrackedManyRelationshipValue(relationshipValueList, relationshipAttr, ref wasAlreadyAttached); - } - } - - // helper method used in GetTrackedRelationshipValue. See comments there. - private IList GetTrackedManyRelationshipValue(IEnumerable relationshipValueList, RelationshipAttribute relationshipAttr, ref bool wasAlreadyAttached) - { - if (relationshipValueList == null) return null; - bool _wasAlreadyAttached = false; - /// if we're not using entity resource separation, we can just read off the related type - /// from the RelationshipAttribute. If we DO use separation, RelationshipAttribute.DependentType - /// will point to the Resource, not the Entity, which is not the one we need here. - bool entityResourceSeparation = relationshipAttr.EntityPropertyName != null; - Type entityType = entityResourceSeparation ? null : relationshipAttr.DependentType; - var trackedPointerCollection = relationshipValueList.Select(pointer => - { - /// todo: we can't just use relationshipAttr.DependentType because - /// this will point to the Resource type in the case of entity resource - /// separation. We should consider to store entity type on - /// the relationship attribute too. - entityType = entityType ?? pointer.GetType(); - var tracked = AttachOrGetTracked(pointer); - if (tracked != null) _wasAlreadyAttached = true; - return Convert.ChangeType(tracked ?? pointer, entityType); - }).ToList().Cast(entityType); - if (_wasAlreadyAttached) wasAlreadyAttached = true; - return (IList)trackedPointerCollection; - } - - // helper method used in GetTrackedRelationshipValue. See comments there. - private IIdentifiable GetTrackedHasOneRelationshipValue(IIdentifiable relationshipValue, HasOneAttribute hasOneAttr, ref bool wasAlreadyAttached) - { - var tracked = AttachOrGetTracked(relationshipValue); - if (tracked != null) wasAlreadyAttached = true; - return tracked ?? relationshipValue; - } - - /// - public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds) - { - // TODO: it would be better to let this be determined within the relationship attribute... - // need to think about the right way to do that since HasMany doesn't need to think about this - // and setting the HasManyThrough.Type to the join type (ArticleTag instead of Tag) for this changes the semantics - // of the property... - var typeToUpdate = (relationship is HasManyThroughAttribute hasManyThrough) - ? hasManyThrough.ThroughType - : relationship.Type; - - var genericProcessor = _genericProcessorFactory.GetProcessor(typeof(GenericProcessor<>), typeToUpdate); - await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds); - } - - - /// - public virtual async Task DeleteAsync(TId id) - { - var entity = await GetAsync(id); - if (entity == null) return false; - _dbSet.Remove(entity); - await _context.SaveChangesAsync(); - return true; - } - - /// - public virtual IQueryable Include(IQueryable entities, string relationshipName) - { - if (string.IsNullOrWhiteSpace(relationshipName)) throw new JsonApiException(400, "Include parameter must not be empty if provided"); - - var relationshipChain = relationshipName.Split('.'); - - // variables mutated in recursive loop - // TODO: make recursive method - string internalRelationshipPath = null; - var entity = _jsonApiContext.RequestEntity; - for (var i = 0; i < relationshipChain.Length; i++) - { - var requestedRelationship = relationshipChain[i]; - var relationship = entity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == requestedRelationship); - if (relationship == null) - { - throw new JsonApiException(400, $"Invalid relationship {requestedRelationship} on {entity.EntityName}", - $"{entity.EntityName} does not have a relationship named {requestedRelationship}"); - } - - if (relationship.CanInclude == false) - { - throw new JsonApiException(400, $"Including the relationship {requestedRelationship} on {entity.EntityName} is not allowed"); - } - - internalRelationshipPath = (internalRelationshipPath == null) - ? relationship.RelationshipPath - : $"{internalRelationshipPath}.{relationship.RelationshipPath}"; - - if (i < relationshipChain.Length) - entity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.Type); - } - - return entities.Include(internalRelationshipPath); - } - - /// - public virtual async Task> PageAsync(IQueryable entities, int pageSize, int pageNumber) - { - if (pageNumber >= 0) - { - // the IQueryable returned from the hook executor is sometimes consumed here. - // In this case, it does not support .ToListAsync(), so we use the method below. - return await this.ToListAsync(entities.PageForward(pageSize, pageNumber)); - } - - // since EntityFramework does not support IQueryable.Reverse(), we need to know the number of queried entities - int numberOfEntities = await this.CountAsync(entities); - - // may be negative - int virtualFirstIndex = numberOfEntities - pageSize * Math.Abs(pageNumber); - int numberOfElementsInPage = Math.Min(pageSize, virtualFirstIndex + pageSize); - - return await ToListAsync(entities - .Skip(virtualFirstIndex) - .Take(numberOfElementsInPage)); - } - - /// - public async Task CountAsync(IQueryable entities) - { - return (entities is IAsyncEnumerable) - ? await entities.CountAsync() - : entities.Count(); - } - - /// - public async Task FirstOrDefaultAsync(IQueryable entities) - { - return (entities is IAsyncEnumerable) - ? await entities.FirstOrDefaultAsync() - : entities.FirstOrDefault(); - } - - /// - public async Task> ToListAsync(IQueryable entities) - { - return (entities is IAsyncEnumerable) - ? await entities.ToListAsync() - : entities.ToList(); - } - - - /// - /// Before assigning new relationship values (UpdateAsync), we need to - /// attach the current database values of the relationship to the dbcontext, else - /// it will not perform a complete-replace which is required for - /// one-to-many and many-to-many. - /// - /// For example: a person `p1` has 2 todoitems: `t1` and `t2`. - /// If we want to update this todoitem set to `t3` and `t4`, simply assigning - /// `p1.todoItems = [t3, t4]` will result in EF Core adding them to the set, - /// resulting in `[t1 ... t4]`. Instead, we should first include `[t1, t2]`, - /// after which the reassignment `p1.todoItems = [t3, t4]` will actually - /// make EF Core perform a complete replace. This method does the loading of `[t1, t2]`. - /// - protected void LoadCurrentRelationships(TEntity oldEntity, RelationshipAttribute relationshipAttribute) - { - if (relationshipAttribute is HasManyThroughAttribute throughAttribute) - { - _context.Entry(oldEntity).Collection(throughAttribute.InternalThroughName).Load(); - - } - else if (relationshipAttribute is HasManyAttribute hasManyAttribute) - { - _context.Entry(oldEntity).Collection(hasManyAttribute.InternalRelationshipName).Load(); - } - } - - /// - /// Assigns the to - /// - private void AssignRelationshipValue(TEntity targetEntity, object relationshipValue, RelationshipAttribute relationshipAttribute) - { - if (relationshipAttribute is HasManyThroughAttribute throughAttribute) - { - // todo: this logic should be put in the HasManyThrough attribute - AssignHasManyThrough(targetEntity, throughAttribute, (IList)relationshipValue); - } - else - { - relationshipAttribute.SetValue(targetEntity, relationshipValue); - } - } - - /// - /// The relationshipValue parameter contains the dependent side of the relationship (Tags). - /// We can't directly add them to the principal entity (Article): we need to - /// use the join table (ArticleTags). This methods assigns the relationship value to entity - /// by taking care of that - /// - private void AssignHasManyThrough(TEntity entity, HasManyThroughAttribute hasManyThrough, IList relationshipValue) - { - var pointers = relationshipValue.Cast(); - var throughRelationshipCollection = Activator.CreateInstance(hasManyThrough.ThroughProperty.PropertyType) as IList; - hasManyThrough.ThroughProperty.SetValue(entity, throughRelationshipCollection); - - foreach (var pointer in pointers) - { - var throughInstance = Activator.CreateInstance(hasManyThrough.ThroughType); - hasManyThrough.LeftProperty.SetValue(throughInstance, entity); - hasManyThrough.RightProperty.SetValue(throughInstance, pointer); - throughRelationshipCollection.Add(throughInstance); - } - } - - /// - /// A helper method that gets the relationship value in the case of - /// entity resource separation. - /// - private IIdentifiable GetEntityResourceSeparationValue(TEntity entity, HasOneAttribute attribute) - { - if (attribute.EntityPropertyName == null) - { - return null; - } - return (IIdentifiable)entity.GetType().GetProperty(attribute.EntityPropertyName)?.GetValue(entity); - } - - /// - /// A helper method that gets the relationship value in the case of - /// entity resource separation. - /// - private IEnumerable GetEntityResourceSeparationValue(TEntity entity, HasManyAttribute attribute) - { - if (attribute.EntityPropertyName == null) - { - return null; - } - return ((IEnumerable)(entity.GetType().GetProperty(attribute.EntityPropertyName)?.GetValue(entity))).Cast(); - } - - /// - /// Given a iidentifiable relationshipvalue, verify if an entity of the underlying - /// type with the same ID is already attached to the dbContext, and if so, return it. - /// If not, attach the relationship value to the dbContext. - /// - /// useful article: https://stackoverflow.com/questions/30987806/dbset-attachentity-vs-dbcontext-entryentity-state-entitystate-modified - /// - private IIdentifiable AttachOrGetTracked(IIdentifiable relationshipValue) - { - var trackedEntity = _context.GetTrackedEntity(relationshipValue); - - if (trackedEntity != null) - { - /// there already was an instance of this type and ID tracked - /// by EF Core. Reattaching will produce a conflict, so from now on we - /// will use the already attached instance instead. This entry might - /// contain updated fields as a result of business logic elsewhere in the application - return trackedEntity; - } - - /// the relationship pointer is new to EF Core, but we are sure - /// it exists in the database, so we attach it. In this case, as per - /// the json:api spec, we can also safely assume that no fields of - /// this entity were updated. - _context.Entry(relationshipValue).State = EntityState.Unchanged; - return null; - } - } -} diff --git a/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs b/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs new file mode 100644 index 0000000000..83f0a3fd28 --- /dev/null +++ b/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs @@ -0,0 +1,428 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Internal.Generics; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query.Internal; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.Data +{ + /// + /// Provides a default repository implementation and is responsible for + /// abstracting any EF Core APIs away from the service layer. + /// + public class DefaultResourceRepository : IResourceRepository + where TResource : class, IIdentifiable + { + private readonly ITargetedFields _targetedFields; + private readonly DbContext _context; + private readonly DbSet _dbSet; + private readonly IResourceGraph _resourceGraph; + private readonly IGenericServiceFactory _genericServiceFactory; + + public DefaultResourceRepository( + ITargetedFields targetedFields, + IDbContextResolver contextResolver, + IResourceGraph resourceGraph, + IGenericServiceFactory genericServiceFactory) + : this(targetedFields, contextResolver, resourceGraph, genericServiceFactory, null) + { } + + public DefaultResourceRepository( + ITargetedFields targetedFields, + IDbContextResolver contextResolver, + IResourceGraph resourceGraph, + IGenericServiceFactory genericServiceFactory, + ILoggerFactory loggerFactory = null) + { + _targetedFields = targetedFields; + _resourceGraph = resourceGraph; + _genericServiceFactory = genericServiceFactory; + _context = contextResolver.GetContext(); + _dbSet = _context.Set(); + } + + /// + public virtual IQueryable Get() => _dbSet; + /// + public virtual IQueryable Get(TId id) => _dbSet.Where(e => e.Id.Equals(id)); + + /// + public virtual IQueryable Select(IQueryable entities, IEnumerable fields = null) + { + if (fields != null && fields.Any()) + return entities.Select(fields); + + return entities; + } + + /// + public virtual IQueryable Filter(IQueryable entities, FilterQueryContext filterQueryContext) + { + if (filterQueryContext.IsCustom) + { + var query = (Func, FilterQuery, IQueryable>)filterQueryContext.CustomQuery; + return query(entities, filterQueryContext.Query); + } + return entities.Filter(filterQueryContext); + } + + /// + public virtual IQueryable Sort(IQueryable entities, SortQueryContext sortQueryContext) + { + return entities.Sort(sortQueryContext); + } + + /// + public virtual async Task CreateAsync(TResource entity) + { + foreach (var relationshipAttr in _targetedFields.Relationships) + { + object trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, entity, out bool relationshipWasAlreadyTracked); + LoadInverseRelationships(trackedRelationshipValue, relationshipAttr); + if (relationshipWasAlreadyTracked || relationshipAttr is HasManyThroughAttribute) + /// We only need to reassign the relationship value to the to-be-added + /// entity when we're using a different instance of the relationship (because this different one + /// was already tracked) than the one assigned to the to-be-created entity. + /// Alternatively, even if we don't have to reassign anything because of already tracked + /// entities, we still need to assign the "through" entities in the case of many-to-many. + relationshipAttr.SetValue(entity, trackedRelationshipValue); + } + _dbSet.Add(entity); + await _context.SaveChangesAsync(); + + // this ensures relationships get reloaded from the database if they have + // been requested. See https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/343 + DetachRelationships(entity); + + return entity; + } + + /// + /// Loads the inverse relationships to prevent foreign key constraints from being violated + /// to support implicit removes, see https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/502. + /// + /// Consider the following example: + /// person.todoItems = [t1,t2] is updated to [t3, t4]. If t3, and/or t4 was + /// already related to a other person, and these persons are NOT loaded in to the + /// db context, then the query may cause a foreign key constraint. Loading + /// these "inverse relationships" into the DB context ensures EF core to take + /// this into account. + /// + /// + private void LoadInverseRelationships(object trackedRelationshipValue, RelationshipAttribute relationshipAttr) + { + if (relationshipAttr.InverseNavigation == null || trackedRelationshipValue == null) return; + if (relationshipAttr is HasOneAttribute hasOneAttr) + { + var relationEntry = _context.Entry((IIdentifiable)trackedRelationshipValue); + if (IsHasOneRelationship(hasOneAttr.InverseNavigation, trackedRelationshipValue.GetType())) + relationEntry.Reference(hasOneAttr.InverseNavigation).Load(); + else + relationEntry.Collection(hasOneAttr.InverseNavigation).Load(); + } + else if (relationshipAttr is HasManyAttribute hasManyAttr && !(relationshipAttr is HasManyThroughAttribute)) + { + foreach (IIdentifiable relationshipValue in (IList)trackedRelationshipValue) + _context.Entry(relationshipValue).Reference(hasManyAttr.InverseNavigation).Load(); + } + } + + private bool IsHasOneRelationship(string internalRelationshipName, Type type) + { + var relationshipAttr = _resourceGraph.GetRelationships(type).FirstOrDefault(r => r.InternalRelationshipName == internalRelationshipName); + if (relationshipAttr != null) + { + if (relationshipAttr is HasOneAttribute) + return true; + + return false; + } + // relationshipAttr is null when we don't put a [RelationshipAttribute] on the inverse navigation property. + // In this case we use relfection to figure out what kind of relationship is pointing back. + return !type.GetProperty(internalRelationshipName).PropertyType.Inherits(typeof(IEnumerable)); + } + + private void DetachRelationships(TResource entity) + { + foreach (var relationship in _targetedFields.Relationships) + { + var value = relationship.GetValue(entity); + if (value == null) + continue; + + if (value is IEnumerable collection) + { + foreach (IIdentifiable single in collection.ToList()) + _context.Entry(single).State = EntityState.Detached; + /// detaching has many relationships is not sufficient to + /// trigger a full reload of relationships: the navigation + /// property actually needs to be nulled out, otherwise + /// EF will still add duplicate instances to the collection + relationship.SetValue(entity, null); + } + else + { + _context.Entry(value).State = EntityState.Detached; + + /// temporary work around for https://github.com/aspnet/EntityFrameworkCore/issues/18621 + /// as soon as ef core 3.1 lands we can get rid of this again. + _context.Entry(entity).State = EntityState.Detached; + } + } + } + + /// + public virtual async Task UpdateAsync(TResource updatedEntity) + { + var databaseEntity = await Get(updatedEntity.Id).FirstOrDefaultAsync(); + if (databaseEntity == null) + return null; + + foreach (var attribute in _targetedFields.Attributes) + attribute.SetValue(databaseEntity, attribute.GetValue(updatedEntity)); + + foreach (var relationshipAttr in _targetedFields.Relationships) + { + /// loads databasePerson.todoItems + LoadCurrentRelationships(databaseEntity, relationshipAttr); + /// trackedRelationshipValue is either equal to updatedPerson.todoItems, + /// or replaced with the same set (same ids) of todoItems from the EF Core change tracker, + /// which is the case if they were already tracked + object trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, updatedEntity, out _); + /// loads into the db context any persons currently related + /// to the todoItems in trackedRelationshipValue + LoadInverseRelationships(trackedRelationshipValue, relationshipAttr); + /// assigns the updated relationship to the database entity + //AssignRelationshipValue(databaseEntity, trackedRelationshipValue, relationshipAttr); + relationshipAttr.SetValue(databaseEntity, trackedRelationshipValue); + } + + await _context.SaveChangesAsync(); + return databaseEntity; + } + + /// + /// Responsible for getting the relationship value for a given relationship + /// attribute of a given entity. It ensures that the relationship value + /// that it returns is attached to the database without reattaching duplicates instances + /// to the change tracker. It does so by checking if there already are + /// instances of the to-be-attached entities in the change tracker. + /// + private object GetTrackedRelationshipValue(RelationshipAttribute relationshipAttr, TResource entity, out bool wasAlreadyAttached) + { + wasAlreadyAttached = false; + if (relationshipAttr is HasOneAttribute hasOneAttr) + { + var relationshipValue = (IIdentifiable)hasOneAttr.GetValue(entity); + if (relationshipValue == null) + return null; + return GetTrackedHasOneRelationshipValue(relationshipValue, ref wasAlreadyAttached); + } + + IEnumerable relationshipValueList = (IEnumerable)relationshipAttr.GetValue(entity); + if (relationshipValueList == null) + return null; + + return GetTrackedManyRelationshipValue(relationshipValueList, relationshipAttr, ref wasAlreadyAttached); + } + + // helper method used in GetTrackedRelationshipValue. See comments below. + private IList GetTrackedManyRelationshipValue(IEnumerable relationshipValueList, RelationshipAttribute relationshipAttr, ref bool wasAlreadyAttached) + { + if (relationshipValueList == null) return null; + bool _wasAlreadyAttached = false; + var trackedPointerCollection = relationshipValueList.Select(pointer => + { // convert each element in the value list to relationshipAttr.DependentType. + var tracked = AttachOrGetTracked(pointer); + if (tracked != null) _wasAlreadyAttached = true; + return Convert.ChangeType(tracked ?? pointer, relationshipAttr.RightType); + }) + .ToList() + .Cast(relationshipAttr.RightType); + if (_wasAlreadyAttached) wasAlreadyAttached = true; + return (IList)trackedPointerCollection; + } + + // helper method used in GetTrackedRelationshipValue. See comments there. + private IIdentifiable GetTrackedHasOneRelationshipValue(IIdentifiable relationshipValue, ref bool wasAlreadyAttached) + { + var tracked = AttachOrGetTracked(relationshipValue); + if (tracked != null) wasAlreadyAttached = true; + return tracked ?? relationshipValue; + } + + /// + public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds) + { + var typeToUpdate = (relationship is HasManyThroughAttribute hasManyThrough) + ? hasManyThrough.ThroughType + : relationship.RightType; + + var helper = _genericServiceFactory.Get(typeof(RepositoryRelationshipUpdateHelper<>), typeToUpdate); + await helper.UpdateRelationshipAsync((IIdentifiable)parent, relationship, relationshipIds); + + await _context.SaveChangesAsync(); + } + + /// + public virtual async Task DeleteAsync(TId id) + { + var entity = await Get(id).FirstOrDefaultAsync(); + if (entity == null) return false; + _dbSet.Remove(entity); + await _context.SaveChangesAsync(); + return true; + } + + public virtual IQueryable Include(IQueryable entities, IEnumerable inclusionChain = null) + { + if (inclusionChain == null || !inclusionChain.Any()) + { + return entities; + } + + string internalRelationshipPath = null; + foreach (var relationship in inclusionChain) + internalRelationshipPath = (internalRelationshipPath == null) + ? relationship.RelationshipPath + : $"{internalRelationshipPath}.{relationship.RelationshipPath}"; + + return entities.Include(internalRelationshipPath); + } + + /// + public virtual async Task> PageAsync(IQueryable entities, int pageSize, int pageNumber) + { + // the IQueryable returned from the hook executor is sometimes consumed here. + // In this case, it does not support .ToListAsync(), so we use the method below. + if (pageNumber >= 0) + { + entities = entities.PageForward(pageSize, pageNumber); + return entities is IAsyncQueryProvider ? await entities.ToListAsync() : entities.ToList(); + } + if (entities is IAsyncEnumerable) + { + // since EntityFramework does not support IQueryable.Reverse(), we need to know the number of queried entities + var totalCount = await entities.CountAsync(); + + int virtualFirstIndex = totalCount - pageSize * Math.Abs(pageNumber); + int numberOfElementsInPage = Math.Min(pageSize, virtualFirstIndex + pageSize); + + return await ToListAsync(entities.Skip(virtualFirstIndex).Take(numberOfElementsInPage)); + } else + { + int firstIndex = pageSize * Math.Abs(pageNumber) - 1; + int numberOfElementsInPage = Math.Min(pageSize, firstIndex + pageSize); + return entities.Reverse().Skip(firstIndex).Take(numberOfElementsInPage); + } + } + + /// + public async Task CountAsync(IQueryable entities) + { + if (entities is IAsyncEnumerable) + { + return await entities.CountAsync(); + } + return entities.Count(); + } + + /// + public async Task FirstOrDefaultAsync(IQueryable entities) + { + return (entities is IAsyncEnumerable) + ? await entities.FirstOrDefaultAsync() + : entities.FirstOrDefault(); + } + + /// + public async Task> ToListAsync(IQueryable entities) + { + if (entities is IAsyncEnumerable) + { + return await entities.ToListAsync(); + } + return entities.ToList(); + } + + /// + /// Before assigning new relationship values (UpdateAsync), we need to + /// attach the current database values of the relationship to the dbcontext, else + /// it will not perform a complete-replace which is required for + /// one-to-many and many-to-many. + /// + /// For example: a person `p1` has 2 todoitems: `t1` and `t2`. + /// If we want to update this todoitem set to `t3` and `t4`, simply assigning + /// `p1.todoItems = [t3, t4]` will result in EF Core adding them to the set, + /// resulting in `[t1 ... t4]`. Instead, we should first include `[t1, t2]`, + /// after which the reassignment `p1.todoItems = [t3, t4]` will actually + /// make EF Core perform a complete replace. This method does the loading of `[t1, t2]`. + /// + protected void LoadCurrentRelationships(TResource oldEntity, RelationshipAttribute relationshipAttribute) + { + if (relationshipAttribute is HasManyThroughAttribute throughAttribute) + { + _context.Entry(oldEntity).Collection(throughAttribute.InternalThroughName).Load(); + } + else if (relationshipAttribute is HasManyAttribute hasManyAttribute) + { + _context.Entry(oldEntity).Collection(hasManyAttribute.InternalRelationshipName).Load(); + } + } + + /// + /// Given a iidentifiable relationshipvalue, verify if an entity of the underlying + /// type with the same ID is already attached to the dbContext, and if so, return it. + /// If not, attach the relationship value to the dbContext. + /// + /// useful article: https://stackoverflow.com/questions/30987806/dbset-attachentity-vs-dbcontext-entryentity-state-entitystate-modified + /// + private IIdentifiable AttachOrGetTracked(IIdentifiable relationshipValue) + { + var trackedEntity = _context.GetTrackedEntity(relationshipValue); + + if (trackedEntity != null) + { + /// there already was an instance of this type and ID tracked + /// by EF Core. Reattaching will produce a conflict, so from now on we + /// will use the already attached instance instead. This entry might + /// contain updated fields as a result of business logic elsewhere in the application + return trackedEntity; + } + + /// the relationship pointer is new to EF Core, but we are sure + /// it exists in the database, so we attach it. In this case, as per + /// the json:api spec, we can also safely assume that no fields of + /// this entity were updated. + _context.Entry(relationshipValue).State = EntityState.Unchanged; + return null; + } + } + + /// + public class DefaultResourceRepository : DefaultResourceRepository, IResourceRepository + where TResource : class, IIdentifiable + { + public DefaultResourceRepository(ITargetedFields targetedFields, + IDbContextResolver contextResolver, + IResourceGraph resourceGraph, + IGenericServiceFactory genericServiceFactory) + : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory) { } + + public DefaultResourceRepository(ITargetedFields targetedFields, + IDbContextResolver contextResolver, + IResourceGraph resourceGraph, + IGenericServiceFactory genericServiceFactory, + ILoggerFactory loggerFactory = null) + : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, loggerFactory) { } + } +} diff --git a/src/JsonApiDotNetCore/Data/IDbContextResolver.cs b/src/JsonApiDotNetCore/Data/IDbContextResolver.cs index 4915d788c4..7d2f5a66dc 100644 --- a/src/JsonApiDotNetCore/Data/IDbContextResolver.cs +++ b/src/JsonApiDotNetCore/Data/IDbContextResolver.cs @@ -6,9 +6,5 @@ namespace JsonApiDotNetCore.Data public interface IDbContextResolver { DbContext GetContext(); - - [Obsolete("Use DbContext.Set() instead", error: true)] - DbSet GetDbSet() - where TEntity : class; } } diff --git a/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs b/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs deleted file mode 100644 index bcbcdb9f8e..0000000000 --- a/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using JsonApiDotNetCore.Internal.Query; -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCore.Data -{ - public interface IEntityReadRepository - : IEntityReadRepository - where TEntity : class, IIdentifiable - { } - - public interface IEntityReadRepository - where TEntity : class, IIdentifiable - { - /// - /// The base GET query. This is a good place to apply rules that should affect all reads, - /// such as authorization of resources. - /// - IQueryable Get(); - - /// - /// Apply fields to the provided queryable - /// - IQueryable Select(IQueryable entities, List fields); - - /// - /// Include a relationship in the query - /// - /// - /// - /// _todoItemsRepository.GetAndIncludeAsync(1, "achieved-date"); - /// - /// - IQueryable Include(IQueryable entities, string relationshipName); - - /// - /// Apply a filter to the provided queryable - /// - IQueryable Filter(IQueryable entities, FilterQuery filterQuery); - - /// - /// Apply a sort to the provided queryable - /// - IQueryable Sort(IQueryable entities, List sortQueries); - - /// - /// Paginate the provided queryable - /// - Task> PageAsync(IQueryable entities, int pageSize, int pageNumber); - - /// - /// Get the entity by id - /// - Task GetAsync(TId id); - - /// - /// Get the entity with the specified id and include the relationship. - /// - /// The entity id - /// The exposed relationship name - /// - /// - /// _todoItemsRepository.GetAndIncludeAsync(1, "achieved-date"); - /// - /// - Task GetAndIncludeAsync(TId id, string relationshipName); - - /// - /// Count the total number of records - /// - Task CountAsync(IQueryable entities); - - /// - /// Get the first element in the collection, return the default value if collection is empty - /// - Task FirstOrDefaultAsync(IQueryable entities); - - /// - /// Convert the collection to a materialized list - /// - Task> ToListAsync(IQueryable entities); - } -} diff --git a/src/JsonApiDotNetCore/Data/IEntityRepository.cs b/src/JsonApiDotNetCore/Data/IEntityRepository.cs deleted file mode 100644 index d4e8870341..0000000000 --- a/src/JsonApiDotNetCore/Data/IEntityRepository.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCore.Data -{ - - public interface IEntityRepository - : IEntityRepository - where TEntity : class, IIdentifiable - { } - - public interface IEntityRepository - : IEntityReadRepository, - IEntityWriteRepository - where TEntity : class, IIdentifiable - { } - - /// - /// A staging interface to avoid breaking changes that - /// specifically depend on EntityFramework. - /// - internal interface IEntityFrameworkRepository - { - /// - /// Ensures that any relationship pointers created during a POST or PATCH - /// request are detached from the DbContext. - /// This allows the relationships to be fully loaded from the database. - /// - /// - /// - /// The only known case when this should be called is when a POST request is - /// sent with an ?include query. - /// - /// See https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/343 - /// - void DetachRelationshipPointers(TEntity entity); - } - -} - - diff --git a/src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs b/src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs deleted file mode 100644 index aa143f53e4..0000000000 --- a/src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCore.Data -{ - public interface IEntityWriteRepository - : IEntityWriteRepository - where TEntity : class, IIdentifiable - { } - - public interface IEntityWriteRepository - where TEntity : class, IIdentifiable - { - Task CreateAsync(TEntity entity); - - Task UpdateAsync(TEntity entity); - - [Obsolete("Use overload UpdateAsync(TEntity updatedEntity): providing parameter ID does no longer add anything relevant")] - Task UpdateAsync(TId id, TEntity entity); - - Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds); - - Task DeleteAsync(TId id); - } -} diff --git a/src/JsonApiDotNetCore/Data/IResourceReadRepository.cs b/src/JsonApiDotNetCore/Data/IResourceReadRepository.cs new file mode 100644 index 0000000000..d2d8946d2d --- /dev/null +++ b/src/JsonApiDotNetCore/Data/IResourceReadRepository.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Data +{ + public interface IResourceReadRepository + : IResourceReadRepository + where TResource : class, IIdentifiable + { } + + public interface IResourceReadRepository + where TResource : class, IIdentifiable + { + /// + /// The base GET query. This is a good place to apply rules that should affect all reads, + /// such as authorization of resources. + /// + IQueryable Get(); + /// + /// Get the entity by id + /// + IQueryable Get(TId id); + /// + /// Apply fields to the provided queryable + /// + IQueryable Select(IQueryable entities, IEnumerable fields); + /// + /// Include a relationship in the query + /// + /// + /// + /// _todoItemsRepository.GetAndIncludeAsync(1, "achieved-date"); + /// + /// + IQueryable Include(IQueryable entities, IEnumerable inclusionChain); + /// + /// Apply a filter to the provided queryable + /// + IQueryable Filter(IQueryable entities, FilterQueryContext filterQuery); + /// + /// Apply a sort to the provided queryable + /// + IQueryable Sort(IQueryable entities, SortQueryContext sortQueries); + /// + /// Paginate the provided queryable + /// + Task> PageAsync(IQueryable entities, int pageSize, int pageNumber); + /// + /// Count the total number of records + /// + Task CountAsync(IQueryable entities); + /// + /// Get the first element in the collection, return the default value if collection is empty + /// + Task FirstOrDefaultAsync(IQueryable entities); + /// + /// Convert the collection to a materialized list + /// + Task> ToListAsync(IQueryable entities); + } +} diff --git a/src/JsonApiDotNetCore/Data/IResourceRepository.cs b/src/JsonApiDotNetCore/Data/IResourceRepository.cs new file mode 100644 index 0000000000..100ea63961 --- /dev/null +++ b/src/JsonApiDotNetCore/Data/IResourceRepository.cs @@ -0,0 +1,15 @@ +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Data +{ + public interface IResourceRepository + : IResourceRepository + where TResource : class, IIdentifiable + { } + + public interface IResourceRepository + : IResourceReadRepository, + IResourceWriteRepository + where TResource : class, IIdentifiable + { } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Data/IResourceWriteRepository.cs b/src/JsonApiDotNetCore/Data/IResourceWriteRepository.cs new file mode 100644 index 0000000000..ea70d70fa8 --- /dev/null +++ b/src/JsonApiDotNetCore/Data/IResourceWriteRepository.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Data +{ + public interface IResourceWriteRepository + : IResourceWriteRepository + where TResource : class, IIdentifiable + { } + + public interface IResourceWriteRepository + where TResource : class, IIdentifiable + { + Task CreateAsync(TResource entity); + + Task UpdateAsync(TResource entity); + + Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds); + + Task DeleteAsync(TId id); + } +} diff --git a/src/JsonApiDotNetCore/DependencyInjection/ServiceLocator.cs b/src/JsonApiDotNetCore/DependencyInjection/ServiceLocator.cs deleted file mode 100644 index 31164ee3b9..0000000000 --- a/src/JsonApiDotNetCore/DependencyInjection/ServiceLocator.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Threading; - -namespace JsonApiDotNetCore.DependencyInjection -{ - internal class ServiceLocator - { - public static AsyncLocal _scopedProvider = new AsyncLocal(); - public static void Initialize(IServiceProvider serviceProvider) => _scopedProvider.Value = serviceProvider; - - public static object GetService(Type type) - => _scopedProvider.Value != null - ? _scopedProvider.Value.GetService(type) - : throw new InvalidOperationException( - $"Service locator has not been initialized for the current asynchronous flow. Call {nameof(Initialize)} first." - ); - } -} diff --git a/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs b/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs index e90471083c..31ba185125 100644 --- a/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs @@ -1,10 +1,9 @@ using System; -using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using JsonApiDotNetCore.Models; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; @@ -12,10 +11,6 @@ namespace JsonApiDotNetCore.Extensions { public static class DbContextExtensions { - [Obsolete("This is no longer required since the introduction of context.Set", error: false)] - public static DbSet GetDbSet(this DbContext context) where T : class - => context.Set(); - /// /// Get the DbSet when the model type is unknown until runtime /// @@ -114,5 +109,20 @@ private void Proxy(Action func) if(_shouldExecute) func(_transaction); } + + public Task CommitAsync(CancellationToken cancellationToken = default) + { + return _transaction.CommitAsync(cancellationToken); + } + + public Task RollbackAsync(CancellationToken cancellationToken = default) + { + return _transaction.RollbackAsync(cancellationToken); + } + + public ValueTask DisposeAsync() + { + return _transaction.DisposeAsync(); + } } } diff --git a/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs b/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs index e66b51a7a0..5f4aeb53dd 100644 --- a/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs @@ -1,6 +1,7 @@ using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Middleware; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -12,30 +13,35 @@ namespace JsonApiDotNetCore.Extensions // ReSharper disable once InconsistentNaming public static class IApplicationBuilderExtensions { - public static IApplicationBuilder UseJsonApi(this IApplicationBuilder app, bool useMvc = true) + /// + /// Adds necessary components such as routing to your application + /// + /// + /// + public static void UseJsonApi(this IApplicationBuilder app) { DisableDetailedErrorsIfProduction(app); LogResourceGraphValidations(app); - - app.UseMiddleware(); - - if (useMvc) - app.UseMvc(); - using (var scope = app.ApplicationServices.CreateScope()) { var inverseRelationshipResolver = scope.ServiceProvider.GetService(); inverseRelationshipResolver?.Resolve(); } - return app; + // An endpoint is selected and set on the HttpContext if a match is found + app.UseRouting(); + + // middleware to run after routing occurs. + app.UseMiddleware(); + + // Executes the endpoints that was selected by routing. + app.UseEndpoints(endpoints => endpoints.MapControllers()); } private static void DisableDetailedErrorsIfProduction(IApplicationBuilder app) { - var environment = (IHostingEnvironment)app.ApplicationServices.GetService(typeof(IHostingEnvironment)); - - if (environment.IsProduction()) + var webHostEnvironment = (IWebHostEnvironment) app.ApplicationServices.GetService(typeof(IWebHostEnvironment)); + if (webHostEnvironment.EnvironmentName == "Production") { JsonApiOptions.DisableErrorStackTraces = true; JsonApiOptions.DisableErrorSource = true; diff --git a/src/JsonApiDotNetCore/Extensions/IEnumerableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IEnumerableExtensions.cs new file mode 100644 index 0000000000..b0748f3eeb --- /dev/null +++ b/src/JsonApiDotNetCore/Extensions/IEnumerableExtensions.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Query; + +namespace JsonApiDotNetCore.Extensions +{ + public static class IEnumerableExtensions + { + /// + /// gets the first element of type if it exists and casts the result to that. + /// Returns null otherwise. + /// + public static TImplementedService FirstOrDefault(this IEnumerable data) where TImplementedService : class, IQueryParameterService + { + return data.FirstOrDefault(qp => qp is TImplementedService) as TImplementedService; + } + } +} diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 55bb80b740..49f6dfc326 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -7,7 +7,6 @@ using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; namespace JsonApiDotNetCore.Extensions { @@ -30,46 +29,57 @@ private static MethodInfo ContainsMethod } } - public static IQueryable Sort(this IQueryable source, IJsonApiContext jsonApiContext, List sortQueries) + public static IQueryable PageForward(this IQueryable source, int pageSize, int pageNumber) { - if (sortQueries == null || sortQueries.Count == 0) - return source; - - var orderedEntities = source.Sort(jsonApiContext, sortQueries[0]); + if (pageSize > 0) + { + if (pageNumber == 0) + pageNumber = 1; - if (sortQueries.Count <= 1) - return orderedEntities; + if (pageNumber > 0) + return source + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize); + } - for (var i = 1; i < sortQueries.Count; i++) - orderedEntities = orderedEntities.Sort(jsonApiContext, sortQueries[i]); + return source; + } - return orderedEntities; + public static void ForEach(this IEnumerable enumeration, Action action) + { + foreach (T item in enumeration) + { + action(item); + } } - public static IOrderedQueryable Sort(this IQueryable source, IJsonApiContext jsonApiContext, SortQuery sortQuery) + public static IQueryable Filter(this IQueryable source, FilterQueryContext filterQuery) { - BaseAttrQuery attr; - if (sortQuery.IsAttributeOfRelationship) - attr = new RelatedAttrSortQuery(jsonApiContext, sortQuery); - else - attr = new AttrSortQuery(jsonApiContext, sortQuery); + if (filterQuery == null) + return source; + + if (filterQuery.Operation == FilterOperation.@in || filterQuery.Operation == FilterOperation.nin) + return CallGenericWhereContainsMethod(source, filterQuery); + + return CallGenericWhereMethod(source, filterQuery); + } + public static IQueryable Select(this IQueryable source, IEnumerable columns) + => CallGenericSelectMethod(source, columns.Select(attr => attr.InternalAttributeName).ToList()); + + public static IOrderedQueryable Sort(this IQueryable source, SortQueryContext sortQuery) + { return sortQuery.Direction == SortDirection.Descending - ? source.OrderByDescending(attr.GetPropertyPath()) - : source.OrderBy(attr.GetPropertyPath()); + ? source.OrderByDescending(sortQuery.GetPropertyPath()) + : source.OrderBy(sortQuery.GetPropertyPath()); } - public static IOrderedQueryable Sort(this IOrderedQueryable source, IJsonApiContext jsonApiContext, SortQuery sortQuery) + public static IOrderedQueryable Sort(this IOrderedQueryable source, SortQueryContext sortQuery) { - BaseAttrQuery attr; - if (sortQuery.IsAttributeOfRelationship) - attr = new RelatedAttrSortQuery(jsonApiContext, sortQuery); - else - attr = new AttrSortQuery(jsonApiContext, sortQuery); return sortQuery.Direction == SortDirection.Descending - ? source.ThenByDescending(attr.GetPropertyPath()) - : source.ThenBy(attr.GetPropertyPath()); + ? source.ThenByDescending(sortQuery.GetPropertyPath()) + : source.ThenBy(sortQuery.GetPropertyPath()); } public static IOrderedQueryable OrderBy(this IQueryable source, string propertyName) @@ -113,62 +123,39 @@ private static IOrderedQueryable CallGenericOrderMethod(IQuery return (IOrderedQueryable)result; } - public static IQueryable Filter(this IQueryable source, IJsonApiContext jsonApiContext, FilterQuery filterQuery) - { - if (filterQuery == null) - return source; - - // Relationship.Attribute - if (filterQuery.IsAttributeOfRelationship) - return source.Filter(new RelatedAttrFilterQuery(jsonApiContext, filterQuery)); - - return source.Filter(new AttrFilterQuery(jsonApiContext, filterQuery)); - } - - public static IQueryable Filter(this IQueryable source, BaseFilterQuery filterQuery) - { - if (filterQuery == null) - return source; - - if (filterQuery.FilterOperation == FilterOperations.@in || filterQuery.FilterOperation == FilterOperations.nin) - return CallGenericWhereContainsMethod(source, filterQuery); - else - return CallGenericWhereMethod(source, filterQuery); - } - - private static Expression GetFilterExpressionLambda(Expression left, Expression right, FilterOperations operation) + private static Expression GetFilterExpressionLambda(Expression left, Expression right, FilterOperation operation) { Expression body; switch (operation) { - case FilterOperations.eq: + case FilterOperation.eq: // {model.Id == 1} body = Expression.Equal(left, right); break; - case FilterOperations.lt: + case FilterOperation.lt: // {model.Id < 1} body = Expression.LessThan(left, right); break; - case FilterOperations.gt: + case FilterOperation.gt: // {model.Id > 1} body = Expression.GreaterThan(left, right); break; - case FilterOperations.le: + case FilterOperation.le: // {model.Id <= 1} body = Expression.LessThanOrEqual(left, right); break; - case FilterOperations.ge: + case FilterOperation.ge: // {model.Id >= 1} body = Expression.GreaterThanOrEqual(left, right); break; - case FilterOperations.like: + case FilterOperation.like: body = Expression.Call(left, "Contains", null, right); break; // {model.Id != 1} - case FilterOperations.ne: + case FilterOperation.ne: body = Expression.NotEqual(left, right); break; - case FilterOperations.isnotnull: + case FilterOperation.isnotnull: // {model.Id != null} if (left.Type.IsValueType && !(left.Type.IsGenericType && left.Type.GetGenericTypeDefinition() == typeof(Nullable<>))) @@ -181,7 +168,7 @@ private static Expression GetFilterExpressionLambda(Expression left, Expression body = Expression.NotEqual(left, right); } break; - case FilterOperations.isnull: + case FilterOperation.isnull: // {model.Id == null} if (left.Type.IsValueType && !(left.Type.IsGenericType && left.Type.GetGenericTypeDefinition() == typeof(Nullable<>))) @@ -201,14 +188,14 @@ private static Expression GetFilterExpressionLambda(Expression left, Expression return body; } - private static IQueryable CallGenericWhereContainsMethod(IQueryable source, BaseFilterQuery filter) + private static IQueryable CallGenericWhereContainsMethod(IQueryable source, FilterQueryContext filter) { var concreteType = typeof(TSource); var property = concreteType.GetProperty(filter.Attribute.InternalAttributeName); try { - var propertyValues = filter.PropertyValue.Split(QueryConstants.COMMA); + var propertyValues = filter.Value.Split(QueryConstants.COMMA); ParameterExpression entity = Expression.Parameter(concreteType, "entity"); MemberExpression member; if (filter.IsAttributeOfRelationship) @@ -222,7 +209,7 @@ private static IQueryable CallGenericWhereContainsMethod(IQuer var method = ContainsMethod.MakeGenericMethod(member.Type); var obj = TypeHelper.ConvertListType(propertyValues, member.Type); - if (filter.FilterOperation == FilterOperations.@in) + if (filter.Operation == FilterOperation.@in) { // Where(i => arr.Contains(i.column)) var contains = Expression.Call(method, new Expression[] { Expression.Constant(obj), member }); @@ -241,7 +228,7 @@ private static IQueryable CallGenericWhereContainsMethod(IQuer } catch (FormatException) { - throw new JsonApiException(400, $"Could not cast {filter.PropertyValue} to {property.PropertyType.Name}"); + throw new JsonApiException(400, $"Could not cast {filter.Value} to {property.PropertyType.Name}"); } } @@ -253,9 +240,9 @@ private static IQueryable CallGenericWhereContainsMethod(IQuer /// /// /// - private static IQueryable CallGenericWhereMethod(IQueryable source, BaseFilterQuery filter) + private static IQueryable CallGenericWhereMethod(IQueryable source, FilterQueryContext filter) { - var op = filter.FilterOperation; + var op = filter.Operation; var concreteType = typeof(TSource); PropertyInfo relationProperty = null; PropertyInfo property = null; @@ -271,7 +258,7 @@ private static IQueryable CallGenericWhereMethod(IQueryable CallGenericWhereMethod(IQueryable 1 - var convertedValue = TypeHelper.ConvertType(filter.PropertyValue, property.PropertyType); + var convertedValue = TypeHelper.ConvertType(filter.Value, property.PropertyType); // {1} right = Expression.Constant(convertedValue, property.PropertyType); } - var body = GetFilterExpressionLambda(left, right, filter.FilterOperation); + var body = GetFilterExpressionLambda(left, right, filter.Operation); var lambda = Expression.Lambda>(body, parameter); return source.Where(lambda); } catch (FormatException) { - throw new JsonApiException(400, $"Could not cast {filter.PropertyValue} to {property.PropertyType.Name}"); + throw new JsonApiException(400, $"Could not cast {filter.Value} to {property.PropertyType.Name}"); } } - public static IQueryable Select(this IQueryable source, List columns) - => CallGenericSelectMethod(source, columns); - private static IQueryable CallGenericSelectMethod(IQueryable source, List columns) { var sourceBindings = new List(); @@ -422,30 +406,5 @@ private static IQueryable CallGenericSelectMethod(IQueryable PageForward(this IQueryable source, int pageSize, int pageNumber) - { - if (pageSize > 0) - { - if (pageNumber == 0) - pageNumber = 1; - - if (pageNumber > 0) - return source - .Skip((pageNumber - 1) * pageSize) - .Take(pageSize); - } - - return source; - } - - public static void ForEach(this IEnumerable enumeration, Action action) - { - foreach (T item in enumeration) - { - action(item); - } - } - } } diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index cb1e635c7a..aab18dec6b 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -2,202 +2,85 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Formatters; using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Generics; -using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Hooks; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCore.Services.Operations; -using JsonApiDotNetCore.Services.Operations.Processors; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Serialization.Client; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Serialization.Server; namespace JsonApiDotNetCore.Extensions { // ReSharper disable once InconsistentNaming public static class IServiceCollectionExtensions { - public static IServiceCollection AddJsonApi(this IServiceCollection services) - where TContext : DbContext - { - var mvcBuilder = services.AddMvcCore(); - return AddJsonApi(services, opt => { }, mvcBuilder); - } - - public static IServiceCollection AddJsonApi(this IServiceCollection services, Action options) - where TContext : DbContext - { - var mvcBuilder = services.AddMvcCore(); - return AddJsonApi(services, options, mvcBuilder); - } - - public static IServiceCollection AddJsonApi( - this IServiceCollection services, - Action options, - IMvcCoreBuilder mvcBuilder) where TContext : DbContext + /// + /// Enabling JsonApiDotNetCore using the EF Core DbContext to build the ResourceGraph. + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddJsonApi(this IServiceCollection services, + Action options = null, + Action resources = null, + IMvcCoreBuilder mvcBuilder = null) + where TEfCoreDbContext : DbContext { - var config = new JsonApiOptions(); - options(config); - config.BuildResourceGraph(builder => builder.AddDbContext()); - - mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, config)); - - AddJsonApiInternals(services, config); + var application = new JsonApiApplicationBuilder(services, mvcBuilder ?? services.AddMvcCore()); + if (options != null) + application.ConfigureJsonApiOptions(options); + application.ConfigureLogging(); + application.ConfigureMvc(); + application.ConfigureResources(resources); + application.ConfigureServices(); return services; } - public static IServiceCollection AddJsonApi( - this IServiceCollection services, - Action configureOptions, - IMvcCoreBuilder mvcBuilder, - Action autoDiscover = null) + /// + /// Enabling JsonApiDotNetCore using manual declaration to build the ResourceGraph. + /// z + /// + /// + /// + /// + public static IServiceCollection AddJsonApi(this IServiceCollection services, + Action options = null, + Action discovery = null, + Action resources = null, + IMvcCoreBuilder mvcBuilder = null) { - var config = new JsonApiOptions(); - configureOptions(config); - - if (autoDiscover != null) - { - var facade = new ServiceDiscoveryFacade(services, config.ResourceGraphBuilder); - autoDiscover(facade); - } - - mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, config)); - - AddJsonApiInternals(services, config); + var application = new JsonApiApplicationBuilder(services, mvcBuilder ?? services.AddMvcCore()); + if (options != null) + application.ConfigureJsonApiOptions(options); + application.ConfigureMvc(); + if (discovery != null) + application.AutoDiscover(discovery); + if (resources != null) + application.ConfigureResources(resources); + application.ConfigureServices(); return services; } - private static void AddMvcOptions(MvcOptions options, JsonApiOptions config) - { - options.Filters.Add(typeof(JsonApiExceptionFilter)); - options.Filters.Add(typeof(TypeMatchFilter)); - options.SerializeAsJsonApi(config); - } - - public static void AddJsonApiInternals( - this IServiceCollection services, - JsonApiOptions jsonApiOptions) where TContext : DbContext - { - if (jsonApiOptions.ResourceGraph == null) - jsonApiOptions.BuildResourceGraph(null); - - services.AddScoped>(); - - AddJsonApiInternals(services, jsonApiOptions); - } - - public static void AddJsonApiInternals( - this IServiceCollection services, - JsonApiOptions jsonApiOptions) + /// + /// Enables client serializers for sending requests and receiving responses + /// in json:api format. Internally only used for testing. + /// Will be extended in the future to be part of a JsonApiClientDotNetCore package. + /// + public static IServiceCollection AddClientSerialization(this IServiceCollection services) { - if (jsonApiOptions.ResourceGraph == null) - jsonApiOptions.ResourceGraph = jsonApiOptions.ResourceGraphBuilder.Build(); - - if (jsonApiOptions.ResourceGraph.UsesDbContext == false) - { - services.AddScoped(); - services.AddSingleton(new DbContextOptionsBuilder().Options); - } - - if (jsonApiOptions.EnableOperations) - AddOperationServices(services); - - services.AddScoped(typeof(IEntityRepository<>), typeof(DefaultEntityRepository<>)); - services.AddScoped(typeof(IEntityRepository<,>), typeof(DefaultEntityRepository<,>)); - - services.AddScoped(typeof(IEntityReadRepository<,>), typeof(DefaultEntityRepository<,>)); - services.AddScoped(typeof(IEntityWriteRepository<,>), typeof(DefaultEntityRepository<,>)); - - - - services.AddScoped(typeof(ICreateService<>), typeof(EntityResourceService<>)); - services.AddScoped(typeof(ICreateService<,>), typeof(EntityResourceService<,>)); - - services.AddScoped(typeof(IGetAllService<>), typeof(EntityResourceService<>)); - services.AddScoped(typeof(IGetAllService<,>), typeof(EntityResourceService<,>)); - - services.AddScoped(typeof(IGetByIdService<>), typeof(EntityResourceService<>)); - services.AddScoped(typeof(IGetByIdService<,>), typeof(EntityResourceService<,>)); - - services.AddScoped(typeof(IGetRelationshipService<,>), typeof(EntityResourceService<>)); - services.AddScoped(typeof(IGetRelationshipService<,>), typeof(EntityResourceService<,>)); - - services.AddScoped(typeof(IUpdateService<>), typeof(EntityResourceService<>)); - services.AddScoped(typeof(IUpdateService<,>), typeof(EntityResourceService<,>)); - - services.AddScoped(typeof(IDeleteService<>), typeof(EntityResourceService<>)); - services.AddScoped(typeof(IDeleteService<,>), typeof(EntityResourceService<,>)); - - services.AddScoped(typeof(IResourceService<>), typeof(EntityResourceService<>)); - services.AddScoped(typeof(IResourceService<,>), typeof(EntityResourceService<,>)); - - services.AddSingleton(jsonApiOptions); - services.AddSingleton(jsonApiOptions.ResourceGraph); - services.AddScoped(); - services.AddSingleton(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(typeof(GenericProcessor<>)); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - - if (jsonApiOptions.EnableResourceHooks) + services.AddSingleton(); + services.AddSingleton(sp => { - services.AddSingleton(typeof(IHooksDiscovery<>), typeof(HooksDiscovery<>)); - services.AddScoped(typeof(IResourceHookContainer<>), typeof(ResourceDefinition<>)); - services.AddTransient(typeof(IResourceHookExecutor), typeof(ResourceHookExecutor)); - services.AddTransient(); - } - - services.AddScoped(); - - } - - private static void AddOperationServices(IServiceCollection services) - { - services.AddScoped(); - - services.AddScoped(typeof(ICreateOpProcessor<>), typeof(CreateOpProcessor<>)); - services.AddScoped(typeof(ICreateOpProcessor<,>), typeof(CreateOpProcessor<,>)); - - services.AddScoped(typeof(IGetOpProcessor<>), typeof(GetOpProcessor<>)); - services.AddScoped(typeof(IGetOpProcessor<,>), typeof(GetOpProcessor<,>)); - - services.AddScoped(typeof(IRemoveOpProcessor<>), typeof(RemoveOpProcessor<>)); - services.AddScoped(typeof(IRemoveOpProcessor<,>), typeof(RemoveOpProcessor<,>)); - - services.AddScoped(typeof(IUpdateOpProcessor<>), typeof(UpdateOpProcessor<>)); - services.AddScoped(typeof(IUpdateOpProcessor<,>), typeof(UpdateOpProcessor<,>)); - - services.AddScoped(); - } - - public static void SerializeAsJsonApi(this MvcOptions options, JsonApiOptions jsonApiOptions) - { - options.InputFormatters.Insert(0, new JsonApiInputFormatter()); - - options.OutputFormatters.Insert(0, new JsonApiOutputFormatter()); - - options.Conventions.Insert(0, new DasherizedRoutingConvention(jsonApiOptions.Namespace)); + var graph = sp.GetService(); + return new RequestSerializer(graph, new ResourceObjectBuilder(graph, new ResourceObjectBuilderSettings())); + }); + return services; } /// @@ -256,7 +139,6 @@ private static HashSet GetResourceTypesFromServiceImplementa } } } - return resourceDecriptors; } } diff --git a/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs b/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs index 023ef09ae2..262e81678e 100644 --- a/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs @@ -1,38 +1,47 @@ using System; +using System.Linq; +using System.Reflection; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.EntityFrameworkCore.Internal; namespace JsonApiDotNetCore.Extensions { public static class ModelStateExtensions { - public static ErrorCollection ConvertToErrorCollection(this ModelStateDictionary modelState, IResourceGraph resourceGraph) + public static ErrorCollection ConvertToErrorCollection(this ModelStateDictionary modelState, Type resourceType) { ErrorCollection collection = new ErrorCollection(); foreach (var entry in modelState) { if (entry.Value.Errors.Any() == false) + { continue; + } - var attrName = resourceGraph.GetPublicAttributeName(entry.Key); + var targetedProperty = resourceType.GetProperty(entry.Key); + var attrName = targetedProperty.GetCustomAttribute().PublicAttributeName; foreach (var modelError in entry.Value.Errors) { if (modelError.Exception is JsonApiException jex) + { collection.Errors.AddRange(jex.GetError().Errors); + } else + { collection.Errors.Add(new Error( status: 422, title: entry.Key, detail: modelError.ErrorMessage, meta: modelError.Exception != null ? ErrorMeta.FromException(modelError.Exception) : null, - source: attrName == null ? null : new { + source: attrName == null ? null : new + { pointer = $"/data/attributes/{attrName}" })); + } } } - return collection; } } diff --git a/src/JsonApiDotNetCore/Extensions/StringExtensions.cs b/src/JsonApiDotNetCore/Extensions/StringExtensions.cs index 24d5bc8d58..1b2bd76c34 100644 --- a/src/JsonApiDotNetCore/Extensions/StringExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/StringExtensions.cs @@ -15,7 +15,7 @@ public static string ToProperCase(this string str) { if ((chars[i]) == '-') { - i = i + 1; + i++; builder.Append(char.ToUpper(chars[i])); } else @@ -50,5 +50,16 @@ public static string Dasherize(this string str) } return str; } + + public static string Camelize(this string str) + { + return char.ToLowerInvariant(str[0]) + str.Substring(1); + } + + public static string NullIfEmpty(this string value) + { + if (value == "") return null; + return value; + } } } diff --git a/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs b/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs index ee79174529..ea0f7ee4ea 100644 --- a/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs @@ -33,7 +33,7 @@ public static void AddRange(this IList list, IEnumerable items) /// /// Extension to use the LINQ cast method in a non-generic way: /// - /// Type targetType = typeof(TEntity) + /// Type targetType = typeof(TResource) /// ((IList)myList).Cast(targetType). /// /// diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs index b456932fc5..6facec6a6a 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs @@ -17,7 +17,6 @@ public bool CanWriteResult(OutputFormatterCanWriteContext context) return string.IsNullOrEmpty(contentTypeString) || contentTypeString == Constants.ContentType; } - public async Task WriteAsync(OutputFormatterWriteContext context) { var writer = context.HttpContext.RequestServices.GetService(); diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs b/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs index b305bf9722..69d04ed3cc 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs @@ -4,58 +4,44 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Services; +using JsonApiDotNetCore.Serialization.Server; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; namespace JsonApiDotNetCore.Formatters { /// public class JsonApiReader : IJsonApiReader { - private readonly IJsonApiDeSerializer _deSerializer; - private readonly IJsonApiContext _jsonApiContext; + private readonly IJsonApiDeserializer _deserializer; private readonly ILogger _logger; - public JsonApiReader(IJsonApiDeSerializer deSerializer, IJsonApiContext jsonApiContext, ILoggerFactory loggerFactory) + public JsonApiReader(IJsonApiDeserializer deserializer, + ILoggerFactory loggerFactory) { - _deSerializer = deSerializer; - _jsonApiContext = jsonApiContext; + _deserializer = deserializer; _logger = loggerFactory.CreateLogger(); } - public Task ReadAsync(InputFormatterContext context) + public async Task ReadAsync(InputFormatterContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); var request = context.HttpContext.Request; if (request.ContentLength == 0) - return InputFormatterResult.SuccessAsync(null); + { + return await InputFormatterResult.SuccessAsync(null); + } try { - var body = GetRequestBody(context.HttpContext.Request.Body); - - object model = null; - - if (_jsonApiContext.IsRelationshipPath) - { - model = _deSerializer.DeserializeRelationship(body); - } - else - { - model = _deSerializer.Deserialize(body); - } - - + var body = await GetRequestBody(context.HttpContext.Request.Body); + object model = _deserializer.Deserialize(body); if (model == null) { _logger?.LogError("An error occurred while de-serializing the payload"); } - if (context.HttpContext.Request.Method == "PATCH") { bool idMissing; @@ -73,13 +59,13 @@ public Task ReadAsync(InputFormatterContext context) throw new JsonApiException(400, "Payload must include id attribute"); } } - return InputFormatterResult.SuccessAsync(model); + return await InputFormatterResult.SuccessAsync(model); } catch (Exception ex) { _logger?.LogError(new EventId(), ex, "An error occurred while de-serializing the payload"); context.ModelState.AddModelError(context.ModelName, ex, context.Metadata); - return InputFormatterResult.FailureAsync(); + return await InputFormatterResult.FailureAsync(); } } @@ -114,14 +100,21 @@ private bool CheckForId(IList modelList) } } return false; - } - private string GetRequestBody(Stream body) + /// + /// Fetches the request from body asynchronously. + /// + /// Input stream for body + /// String content of body sent to server. + private async Task GetRequestBody(Stream body) { using (var reader = new StreamReader(body)) { - return reader.ReadToEnd(); + // This needs to be set to async because + // Synchronous IO operations are + // https://github.com/aspnet/AspNetCore/issues/7644 + return await reader.ReadToEndAsync(); } } } diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs b/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs index fcf0ac7850..d1500e6ae5 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs @@ -2,20 +2,25 @@ using System.Text; using System.Threading.Tasks; using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Server; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; namespace JsonApiDotNetCore.Formatters { + /// + /// Formats the response data used https://docs.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting?view=aspnetcore-3.0. + /// It was intended to have as little dependencies as possible in formatting layer for greater extensibility. + /// It onls depends on . + /// public class JsonApiWriter : IJsonApiWriter { private readonly ILogger _logger; private readonly IJsonApiSerializer _serializer; - public JsonApiWriter( - IJsonApiSerializer serializer, - ILoggerFactory loggerFactory) + public JsonApiWriter(IJsonApiSerializer serializer, + ILoggerFactory loggerFactory) { _serializer = serializer; _logger = loggerFactory.CreateLogger(); @@ -31,28 +36,29 @@ public async Task WriteAsync(OutputFormatterWriteContext context) { response.ContentType = Constants.ContentType; string responseContent; - try + if (_serializer == null) { - responseContent = GetResponseBody(context.Object); + responseContent = JsonConvert.SerializeObject(context.Object); } - catch (Exception e) + else { - _logger?.LogError(new EventId(), e, "An error ocurred while formatting the response"); - responseContent = GetErrorResponse(e); - response.StatusCode = 400; + try + { + responseContent = _serializer.Serialize(context.Object); + } + catch (Exception e) + { + _logger?.LogError(new EventId(), e, "An error ocurred while formatting the response"); + var errors = new ErrorCollection(); + errors.Add(new Error(400, e.Message, ErrorMeta.FromException(e))); + responseContent = _serializer.Serialize(errors); + response.StatusCode = 400; + } } await writer.WriteAsync(responseContent); await writer.FlushAsync(); } } - - private string GetResponseBody(object responseObject) => _serializer.Serialize(responseObject); - private string GetErrorResponse(Exception e) - { - var errors = new ErrorCollection(); - errors.Add(new Error(400, e.Message, ErrorMeta.FromException(e))); - return errors.GetJson(); - } } } diff --git a/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs b/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs deleted file mode 100644 index 6feaab949d..0000000000 --- a/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using Humanizer; -using JsonApiDotNetCore.Models; -using str = JsonApiDotNetCore.Extensions.StringExtensions; - - -namespace JsonApiDotNetCore.Graph -{ - /// - /// Provides an interface for formatting resource names by convention - /// - public interface IResourceNameFormatter - { - /// - /// Get the publicly visible resource name from the internal type name - /// - string FormatResourceName(Type resourceType); - - /// - /// Get the publicly visible name for the given property - /// - string FormatPropertyName(PropertyInfo property); - - /// - /// Aoplies the desired casing convention to the internal string. - /// This is generally applied to the type name after pluralization. - /// - string ApplyCasingConvention(string properName); - } - - public class DefaultResourceNameFormatter : IResourceNameFormatter - { - /// - /// Uses the internal type name to determine the external resource name. - /// By default we us Humanizer for pluralization and then we dasherize the name. - /// - /// - /// - /// _default.FormatResourceName(typeof(TodoItem)).Dump(); - /// // > "todo-items" - /// - /// - public string FormatResourceName(Type type) - { - try - { - // check the class definition first - // [Resource("models"] public class Model : Identifiable { /* ... */ } - if (type.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute) - return attribute.ResourceName; - - return ApplyCasingConvention(type.Name.Pluralize()); - } - catch (InvalidOperationException e) - { - throw new InvalidOperationException($"Cannot define multiple {nameof(ResourceAttribute)}s on type '{type}'.", e); - } - } - - /// - /// Aoplies the desired casing convention to the internal string. - /// This is generally applied to the type name after pluralization. - /// - /// - /// - /// - /// _default.ApplyCasingConvention("TodoItems"); - /// // > "todo-items" - /// - /// _default.ApplyCasingConvention("TodoItem"); - /// // > "todo-item" - /// - /// - public string ApplyCasingConvention(string properName) => str.Dasherize(properName); - - /// - /// Uses the internal PropertyInfo to determine the external resource name. - /// By default the name will be formatted to kebab-case. - /// - /// - /// Given the following property: - /// - /// public string CompoundProperty { get; set; } - /// - /// The public attribute will be formatted like so: - /// - /// _default.FormatPropertyName(compoundProperty).Dump(); - /// // > "compound-property" - /// - /// - public string FormatPropertyName(PropertyInfo property) => str.Dasherize(property.Name); - } -} diff --git a/src/JsonApiDotNetCore/Graph/IServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/IServiceDiscoveryFacade.cs new file mode 100644 index 0000000000..53e4e80cc5 --- /dev/null +++ b/src/JsonApiDotNetCore/Graph/IServiceDiscoveryFacade.cs @@ -0,0 +1,10 @@ +using System.Reflection; + +namespace JsonApiDotNetCore.Graph +{ + public interface IServiceDiscoveryFacade + { + ServiceDiscoveryFacade AddAssembly(Assembly assembly); + ServiceDiscoveryFacade AddCurrentAssembly(); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/CamelCaseFormatter.cs b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/CamelCaseFormatter.cs new file mode 100644 index 0000000000..7e9a4d1e27 --- /dev/null +++ b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/CamelCaseFormatter.cs @@ -0,0 +1,40 @@ +using str = JsonApiDotNetCore.Extensions.StringExtensions; + +namespace JsonApiDotNetCore.Graph +{ + /// + /// Uses kebab-case as formatting options in the route and request/response body. + /// + /// + /// + /// _default.FormatResourceName(typeof(TodoItem)).Dump(); + /// // > "todoItems" + /// + /// + /// + /// Given the following property: + /// + /// public string CompoundProperty { get; set; } + /// + /// The public attribute will be formatted like so: + /// + /// _default.FormatPropertyName(compoundProperty).Dump(); + /// // > "compoundProperty" + /// + /// + /// + /// + /// _default.ApplyCasingConvention("TodoItems"); + /// // > "todoItems" + /// + /// _default.ApplyCasingConvention("TodoItem"); + /// // > "todoItem" + /// + /// + public class CamelCaseFormatter: BaseResourceNameFormatter + { + /// + public override string ApplyCasingConvention(string properName) => str.Camelize(properName); + } +} + diff --git a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/IResourceNameFormatter.cs b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/IResourceNameFormatter.cs new file mode 100644 index 0000000000..9de1a7c6a6 --- /dev/null +++ b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/IResourceNameFormatter.cs @@ -0,0 +1,27 @@ +using System; +using System.Reflection; + +namespace JsonApiDotNetCore.Graph +{ + /// + /// Provides an interface for formatting resource names by convention + /// + public interface IResourceNameFormatter + { + /// + /// Get the publicly visible resource name from the internal type name + /// + string FormatResourceName(Type resourceType); + + /// + /// Get the publicly visible name for the given property + /// + string FormatPropertyName(PropertyInfo property); + + /// + /// Aoplies the desired casing convention to the internal string. + /// This is generally applied to the type name after pluralization. + /// + string ApplyCasingConvention(string properName); + } +} diff --git a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/KebabCaseFormatter.cs b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/KebabCaseFormatter.cs new file mode 100644 index 0000000000..22144a4769 --- /dev/null +++ b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/KebabCaseFormatter.cs @@ -0,0 +1,39 @@ +using str = JsonApiDotNetCore.Extensions.StringExtensions; + +namespace JsonApiDotNetCore.Graph +{ + /// + /// Uses kebab-case as formatting options in the route and request/response body. + /// + /// + /// + /// _default.FormatResourceName(typeof(TodoItem)).Dump(); + /// // > "todo-items" + /// + /// + /// + /// Given the following property: + /// + /// public string CompoundProperty { get; set; } + /// + /// The public attribute will be formatted like so: + /// + /// _default.FormatPropertyName(compoundProperty).Dump(); + /// // > "compound-property" + /// + /// + /// + /// + /// _default.ApplyCasingConvention("TodoItems"); + /// // > "todo-items" + /// + /// _default.ApplyCasingConvention("TodoItem"); + /// // > "todo-item" + /// + /// + public class KebabCaseFormatter : BaseResourceNameFormatter + { + /// + public override string ApplyCasingConvention(string properName) => str.Dasherize(properName); + } +} diff --git a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/ResourceNameFormatterBase.cs b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/ResourceNameFormatterBase.cs new file mode 100644 index 0000000000..319824041d --- /dev/null +++ b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/ResourceNameFormatterBase.cs @@ -0,0 +1,42 @@ +using System; +using System.Reflection; +using Humanizer; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Graph +{ + public abstract class BaseResourceNameFormatter : IResourceNameFormatter + { + /// + /// Uses the internal type name to determine the external resource name. + /// + public string FormatResourceName(Type type) + { + try + { + // check the class definition first + // [Resource("models"] public class Model : Identifiable { /* ... */ } + if (type.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute) + return attribute.ResourceName; + + return ApplyCasingConvention(type.Name.Pluralize()); + } + catch (InvalidOperationException e) + { + throw new InvalidOperationException($"Cannot define multiple {nameof(ResourceAttribute)}s on type '{type}'.", e); + } + } + + /// + /// Aoplies the desired casing convention to the internal string. + /// This is generally applied to the type name after pluralization. + /// + public abstract string ApplyCasingConvention(string properName); + + /// + /// Uses the internal PropertyInfo to determine the external resource name. + /// By default the name will be formatted to kebab-case. + /// + public string FormatPropertyName(PropertyInfo property) => ApplyCasingConvention(property.Name); + } +} diff --git a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs index 99c3845108..074914faa3 100644 --- a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs @@ -1,5 +1,4 @@ using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Data; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; @@ -13,11 +12,11 @@ namespace JsonApiDotNetCore.Graph { - public class ServiceDiscoveryFacade + public class ServiceDiscoveryFacade : IServiceDiscoveryFacade { internal static HashSet ServiceInterfaces = new HashSet { typeof(IResourceService<>), - typeof(IResourceService<,>), + typeof(IResourceService<,>), typeof(IResourceCmdService<>), typeof(IResourceCmdService<,>), typeof(IResourceQueryService<>), @@ -39,24 +38,21 @@ public class ServiceDiscoveryFacade }; internal static HashSet RepositoryInterfaces = new HashSet { - typeof(IEntityRepository<>), - typeof(IEntityRepository<,>), - typeof(IEntityWriteRepository<>), - typeof(IEntityWriteRepository<,>), - typeof(IEntityReadRepository<>), - typeof(IEntityReadRepository<,>) + typeof(IResourceRepository<>), + typeof(IResourceRepository<,>), + typeof(IResourceWriteRepository<>), + typeof(IResourceWriteRepository<,>), + typeof(IResourceReadRepository<>), + typeof(IResourceReadRepository<,>) }; - private readonly IServiceCollection _services; - private readonly IResourceGraphBuilder _graphBuilder; + private readonly IResourceGraphBuilder _resourceGraphBuilder; private readonly List _identifiables = new List(); - public ServiceDiscoveryFacade( - IServiceCollection services, - IResourceGraphBuilder graphBuilder) + public ServiceDiscoveryFacade(IServiceCollection services, IResourceGraphBuilder resourceGraphBuilder) { _services = services; - _graphBuilder = graphBuilder; + _resourceGraphBuilder = resourceGraphBuilder; } /// @@ -79,14 +75,28 @@ public ServiceDiscoveryFacade AddAssembly(Assembly assembly) AddServices(assembly, resourceDescriptor); AddRepositories(assembly, resourceDescriptor); } - return this; } + + public IEnumerable FindDerivedTypes(Type baseType) + { + return baseType.Assembly.GetTypes().Where(t => + { + if (t.BaseType != null) + { + return baseType.IsSubclassOf(t); + } + return false; + + }); + } + + private void AddDbContextResolvers(Assembly assembly) { var dbContextTypes = TypeLocator.GetDerivedTypes(assembly, typeof(DbContext)); - foreach(var dbContextType in dbContextTypes) + foreach (var dbContextType in dbContextTypes) { var resolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); _services.AddScoped(typeof(IDbContextResolver), resolverType); @@ -94,7 +104,7 @@ private void AddDbContextResolvers(Assembly assembly) } /// - /// Adds resources to the graph and registers types on the container. + /// Adds resources to the resourceGraph and registers types on the container. /// /// The assembly to search for resources in. public ServiceDiscoveryFacade AddResources(Assembly assembly) @@ -125,17 +135,17 @@ private void RegisterResourceDefinition(Assembly assembly, ResourceDescriptor id catch (InvalidOperationException e) { throw new JsonApiSetupException($"Cannot define multiple ResourceDefinition<> implementations for '{identifiable.ResourceType}'", e); - } + } } private void AddResourceToGraph(ResourceDescriptor identifiable) { var resourceName = FormatResourceName(identifiable.ResourceType); - _graphBuilder.AddResource(identifiable.ResourceType, identifiable.IdType, resourceName); + _resourceGraphBuilder.AddResource(identifiable.ResourceType, identifiable.IdType, resourceName); } - private string FormatResourceName(Type resourceType) - => JsonApiOptions.ResourceNameFormatter.FormatResourceName(resourceType); + private string FormatResourceName(Type resourceType) + => new KebabCaseFormatter().FormatResourceName(resourceType); /// /// Add implementations to container. @@ -145,45 +155,59 @@ public ServiceDiscoveryFacade AddServices(Assembly assembly) { var resourceDescriptors = TypeLocator.GetIdentifableTypes(assembly); foreach (var resourceDescriptor in resourceDescriptors) + { AddServices(assembly, resourceDescriptor); - + } return this; } private void AddServices(Assembly assembly, ResourceDescriptor resourceDescriptor) { - foreach(var serviceInterface in ServiceInterfaces) + foreach (var serviceInterface in ServiceInterfaces) + { RegisterServiceImplementations(assembly, serviceInterface, resourceDescriptor); + } } /// - /// Add implementations to container. + /// Add implementations to container. /// /// The assembly to search for resources in. - public ServiceDiscoveryFacade AddRepositories(Assembly assembly) + public ServiceDiscoveryFacade AddRepositories(Assembly assembly) { var resourceDescriptors = TypeLocator.GetIdentifableTypes(assembly); foreach (var resourceDescriptor in resourceDescriptors) + { AddRepositories(assembly, resourceDescriptor); + } return this; } private void AddRepositories(Assembly assembly, ResourceDescriptor resourceDescriptor) { - foreach(var serviceInterface in RepositoryInterfaces) + foreach (var serviceInterface in RepositoryInterfaces) + { RegisterServiceImplementations(assembly, serviceInterface, resourceDescriptor); + } } - + public int i = 0; private void RegisterServiceImplementations(Assembly assembly, Type interfaceType, ResourceDescriptor resourceDescriptor) { - var genericArguments = interfaceType.GetTypeInfo().GenericTypeParameters.Length == 2 - ? new [] { resourceDescriptor.ResourceType, resourceDescriptor.IdType } - : new [] { resourceDescriptor.ResourceType }; - + if (resourceDescriptor.IdType == typeof(Guid) && interfaceType.GetTypeInfo().GenericTypeParameters.Length == 1) + { + return; + } + var genericArguments = interfaceType.GetTypeInfo().GenericTypeParameters.Length == 2 ? new[] { resourceDescriptor.ResourceType, resourceDescriptor.IdType } : new[] { resourceDescriptor.ResourceType }; var service = TypeLocator.GetGenericInterfaceImplementation(assembly, interfaceType, genericArguments); + //if(service.implementation?.Name == "CustomArticleService" && genericArguments[0].Name != "Article") + //{ + // service = TypeLocator.GetGenericInterfaceImplementation(assembly, interfaceType, genericArguments); + //} if (service.implementation != null) + { _services.AddScoped(service.registrationInterface, service.implementation); + } } } } diff --git a/src/JsonApiDotNetCore/Graph/TypeLocator.cs b/src/JsonApiDotNetCore/Graph/TypeLocator.cs index 610b813428..1e82e438c3 100644 --- a/src/JsonApiDotNetCore/Graph/TypeLocator.cs +++ b/src/JsonApiDotNetCore/Graph/TypeLocator.cs @@ -14,7 +14,7 @@ static class TypeLocator private static Dictionary _typeCache = new Dictionary(); private static Dictionary> _identifiableTypeCache = new Dictionary>(); - + /// /// Determine whether or not this is a json:api resource by checking if it implements . /// Returns the status and the resultant id type, either `(true, Type)` OR `(false, null)` @@ -48,10 +48,10 @@ private static Type[] GetAssemblyTypes(Assembly assembly) /// /// Get all implementations of in the assembly /// - public static IEnumerable GetIdentifableTypes(Assembly assembly) + public static IEnumerable GetIdentifableTypes(Assembly assembly) => (_identifiableTypeCache.TryGetValue(assembly, out var descriptors) == false) ? FindIdentifableTypes(assembly) - : _identifiableTypeCache[assembly]; + : _identifiableTypeCache[assembly]; private static IEnumerable FindIdentifableTypes(Assembly assembly) { @@ -60,7 +60,7 @@ private static IEnumerable FindIdentifableTypes(Assembly ass foreach (var type in assembly.GetTypes()) { - if (TryGetResourceDescriptor(type, out var descriptor)) + if (TryGetResourceDescriptor(type, out var descriptor)) { descriptors.Add(descriptor); yield return descriptor; @@ -77,15 +77,15 @@ private static IEnumerable FindIdentifableTypes(Assembly ass internal static bool TryGetResourceDescriptor(Type type, out ResourceDescriptor descriptor) { var possible = GetIdType(type); - if (possible.isJsonApiResource) { + if (possible.isJsonApiResource) + { descriptor = new ResourceDescriptor(type, possible.idType); return true; - } - + } + descriptor = ResourceDescriptor.Empty; return false; } - /// /// Get all implementations of the generic interface /// @@ -99,11 +99,11 @@ internal static bool TryGetResourceDescriptor(Type type, out ResourceDescriptor /// public static (Type implementation, Type registrationInterface) GetGenericInterfaceImplementation(Assembly assembly, Type openGenericInterfaceType, params Type[] genericInterfaceArguments) { - if(assembly == null) throw new ArgumentNullException(nameof(assembly)); - if(openGenericInterfaceType == null) throw new ArgumentNullException(nameof(openGenericInterfaceType)); - if(genericInterfaceArguments == null) throw new ArgumentNullException(nameof(genericInterfaceArguments)); - if(genericInterfaceArguments.Length == 0) throw new ArgumentException("No arguments supplied for the generic interface.", nameof(genericInterfaceArguments)); - if(openGenericInterfaceType.IsGenericType == false) throw new ArgumentException("Requested type is not a generic type.", nameof(openGenericInterfaceType)); + if (assembly == null) throw new ArgumentNullException(nameof(assembly)); + if (openGenericInterfaceType == null) throw new ArgumentNullException(nameof(openGenericInterfaceType)); + if (genericInterfaceArguments == null) throw new ArgumentNullException(nameof(genericInterfaceArguments)); + if (genericInterfaceArguments.Length == 0) throw new ArgumentException("No arguments supplied for the generic interface.", nameof(genericInterfaceArguments)); + if (openGenericInterfaceType.IsGenericType == false) throw new ArgumentException("Requested type is not a generic type.", nameof(openGenericInterfaceType)); foreach (var type in assembly.GetTypes()) { @@ -113,7 +113,8 @@ public static (Type implementation, Type registrationInterface) GetGenericInterf if (interfaceType.IsGenericType) { var genericTypeDefinition = interfaceType.GetGenericTypeDefinition(); - if(genericTypeDefinition == openGenericInterfaceType.GetGenericTypeDefinition()) { + if (interfaceType.GetGenericArguments().First() == genericInterfaceArguments.First() &&genericTypeDefinition == openGenericInterfaceType.GetGenericTypeDefinition()) + { return ( type, genericTypeDefinition.MakeGenericType(genericInterfaceArguments) @@ -157,7 +158,7 @@ public static IEnumerable GetDerivedTypes(Assembly assembly, Type inherite { foreach (var type in assembly.GetTypes()) { - if(inheritedType.IsAssignableFrom(type)) + if (inheritedType.IsAssignableFrom(type)) yield return type; } } diff --git a/src/JsonApiDotNetCore/Hooks/Discovery/HooksDiscovery.cs b/src/JsonApiDotNetCore/Hooks/Discovery/HooksDiscovery.cs index 958a3e3ab2..9b95e24562 100644 --- a/src/JsonApiDotNetCore/Hooks/Discovery/HooksDiscovery.cs +++ b/src/JsonApiDotNetCore/Hooks/Discovery/HooksDiscovery.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Graph; -using JsonApiDotNetCore.Hooks; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; @@ -11,7 +10,7 @@ namespace JsonApiDotNetCore.Hooks /// /// The default implementation for IHooksDiscovery /// - public class HooksDiscovery : IHooksDiscovery where TEntity : class, IIdentifiable + public class HooksDiscovery : IHooksDiscovery where TResource : class, IIdentifiable { private readonly ResourceHook[] _allHooks; private readonly ResourceHook[] _databaseValuesAttributeAllowed = @@ -41,8 +40,8 @@ public HooksDiscovery() /// The implemented hooks for model. void DiscoverImplementedHooksForModel() { - Type parameterizedResourceDefinition = typeof(ResourceDefinition); - var derivedTypes = TypeLocator.GetDerivedTypes(typeof(TEntity).Assembly, parameterizedResourceDefinition).ToList(); + Type parameterizedResourceDefinition = typeof(ResourceDefinition); + var derivedTypes = TypeLocator.GetDerivedTypes(typeof(TResource).Assembly, parameterizedResourceDefinition).ToList(); var implementedHooks = new List(); @@ -66,7 +65,7 @@ void DiscoverImplementedHooksForModel() if (method.DeclaringType != parameterizedResourceDefinition) { implementedHooks.Add(hook); - var attr = method.GetCustomAttributes(true).OfType().SingleOrDefault(); + var attr = method.GetCustomAttributes(true).OfType().SingleOrDefault(); if (attr != null) { if (!_databaseValuesAttributeAllowed.Contains(hook)) diff --git a/src/JsonApiDotNetCore/Hooks/Discovery/IHooksDiscovery.cs b/src/JsonApiDotNetCore/Hooks/Discovery/IHooksDiscovery.cs index 709b30900e..9db0d2a0ca 100644 --- a/src/JsonApiDotNetCore/Hooks/Discovery/IHooksDiscovery.cs +++ b/src/JsonApiDotNetCore/Hooks/Discovery/IHooksDiscovery.cs @@ -4,11 +4,11 @@ namespace JsonApiDotNetCore.Hooks { /// - /// A singleton service for a particular TEntity that stores a field of + /// A singleton service for a particular TResource that stores a field of /// enums that represents which resource hooks have been implemented for that /// particular entity. /// - public interface IHooksDiscovery : IHooksDiscovery where TEntity : class, IIdentifiable + public interface IHooksDiscovery : IHooksDiscovery where TResource : class, IIdentifiable { } @@ -17,7 +17,7 @@ public interface IHooksDiscovery : IHooksDiscovery where TEntity : clas public interface IHooksDiscovery { /// - /// A list of the implemented hooks for resource TEntity + /// A list of the implemented hooks for resource TResource /// /// The implemented hooks. ResourceHook[] ImplementedHooks { get; } diff --git a/src/JsonApiDotNetCore/Hooks/Discovery/LoadDatabaseValuesAttribute.cs b/src/JsonApiDotNetCore/Hooks/Discovery/LoadDatabaseValuesAttribute.cs index 6a47e9d2a0..1477cc0ec1 100644 --- a/src/JsonApiDotNetCore/Hooks/Discovery/LoadDatabaseValuesAttribute.cs +++ b/src/JsonApiDotNetCore/Hooks/Discovery/LoadDatabaseValuesAttribute.cs @@ -1,10 +1,10 @@ using System; namespace JsonApiDotNetCore.Hooks { - public class LoadDatabaseValues : Attribute + public class LoaDatabaseValues : Attribute { public readonly bool value; - public LoadDatabaseValues(bool mode = true) + public LoaDatabaseValues(bool mode = true) { value = mode; } diff --git a/src/JsonApiDotNetCore/Hooks/Execution/DiffableEntityHashSet.cs b/src/JsonApiDotNetCore/Hooks/Execution/DiffableEntityHashSet.cs index 6c658fe3ba..c43ae530c4 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/DiffableEntityHashSet.cs +++ b/src/JsonApiDotNetCore/Hooks/Execution/DiffableEntityHashSet.cs @@ -7,7 +7,7 @@ using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; +using JsonApiDotNetCore.Serialization; namespace JsonApiDotNetCore.Hooks { @@ -52,9 +52,9 @@ public DiffableEntityHashSet(HashSet requestEntities, internal DiffableEntityHashSet(IEnumerable requestEntities, IEnumerable databaseEntities, Dictionary relationships, - IJsonApiContext jsonApiContext) + ITargetedFields targetedFields) : this((HashSet)requestEntities, (HashSet)databaseEntities, TypeHelper.ConvertRelationshipDictionary(relationships), - TypeHelper.ConvertAttributeDictionary(jsonApiContext.AttributesToUpdate, (HashSet)requestEntities)) + TypeHelper.ConvertAttributeDictionary(targetedFields.Attributes, (HashSet)requestEntities)) { } @@ -90,7 +90,7 @@ public IEnumerable> GetDiffs() private HashSet ThrowNoDbValuesError() { - throw new MemberAccessException("Cannot iterate over the diffs if the LoadDatabaseValues option is set to false"); + throw new MemberAccessException("Cannot iterate over the diffs if the LoaDatabaseValues option is set to false"); } } diff --git a/src/JsonApiDotNetCore/Hooks/Execution/EntityHashSet.cs b/src/JsonApiDotNetCore/Hooks/Execution/EntityHashSet.cs index a6302a40c7..93ee588a7a 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/EntityHashSet.cs +++ b/src/JsonApiDotNetCore/Hooks/Execution/EntityHashSet.cs @@ -3,8 +3,6 @@ using System.Collections; using JsonApiDotNetCore.Internal; using System; -using System.Collections.ObjectModel; -using System.Collections.Immutable; using System.Linq.Expressions; namespace JsonApiDotNetCore.Hooks @@ -27,8 +25,6 @@ public interface IEntityHashSet : IByAffectedRelationships /// public class EntityHashSet : HashSet, IEntityHashSet where TResource : class, IIdentifiable { - - /// public Dictionary> AffectedRelationships { get => _relationships; } private readonly RelationshipsDictionary _relationships; @@ -48,15 +44,15 @@ internal EntityHashSet(IEnumerable entities, /// - public Dictionary> GetByRelationship(Type principalType) + public Dictionary> GetByRelationship(Type leftType) { - return _relationships.GetByRelationship(principalType); + return _relationships.GetByRelationship(leftType); } /// - public Dictionary> GetByRelationship() where TRelatedResource : class, IIdentifiable + public Dictionary> GetByRelationship() where TRightResource : class, IIdentifiable { - return GetByRelationship(typeof(TRelatedResource)); + return GetByRelationship(typeof(TRightResource)); } /// diff --git a/src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs b/src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs index df7f4daa0c..5e2ee7731d 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -7,49 +7,44 @@ using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Extensions; -using PrincipalType = System.Type; -using DependentType = System.Type; -using Microsoft.EntityFrameworkCore; -using JsonApiDotNetCore.Services; +using LeftType = System.Type; +using RightType = System.Type; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Configuration; namespace JsonApiDotNetCore.Hooks { /// internal class HookExecutorHelper : IHookExecutorHelper { - protected readonly IGenericProcessorFactory _genericProcessorFactory; - protected readonly IResourceGraph _graph; - protected readonly Dictionary _hookContainers; - protected readonly Dictionary _hookDiscoveries; + private readonly IdentifiableComparer _comparer = new IdentifiableComparer(); + private readonly IJsonApiOptions _options; + protected readonly IGenericServiceFactory _genericProcessorFactory; + protected readonly Dictionary _hookContainers; + protected readonly Dictionary _hookDiscoveries; protected readonly List _targetedHooksForRelatedEntities; - protected readonly IJsonApiContext _context; - public HookExecutorHelper( - IGenericProcessorFactory genericProcessorFactory, - IResourceGraph graph, - IJsonApiContext context - ) + public HookExecutorHelper(IGenericServiceFactory genericProcessorFactory, + IJsonApiOptions options) { + _options = options; _genericProcessorFactory = genericProcessorFactory; - _graph = graph; - _context = context; - _hookContainers = new Dictionary(); - _hookDiscoveries = new Dictionary(); + _hookContainers = new Dictionary(); + _hookDiscoveries = new Dictionary(); _targetedHooksForRelatedEntities = new List(); } /// - public IResourceHookContainer GetResourceHookContainer(DependentType dependentType, ResourceHook hook = ResourceHook.None) + public IResourceHookContainer GetResourceHookContainer(RightType rightType, ResourceHook hook = ResourceHook.None) { /// checking the cache if we have a reference for the requested container, /// regardless of the hook we will use it for. If the value is null, /// it means there was no implementation IResourceHookContainer at all, /// so we need not even bother. - if (!_hookContainers.TryGetValue(dependentType, out IResourceHookContainer container)) + if (!_hookContainers.TryGetValue(rightType, out IResourceHookContainer container)) { - container = (_genericProcessorFactory.GetProcessor(typeof(ResourceDefinition<>), dependentType)); - _hookContainers[dependentType] = container; + container = (_genericProcessorFactory.Get(typeof(ResourceDefinition<>), rightType)); + _hookContainers[rightType] = container; } if (container == null) return container; @@ -68,59 +63,49 @@ public IResourceHookContainer GetResourceHookContainer(DependentType dependentTy foreach (ResourceHook targetHook in targetHooks) { - if (ShouldExecuteHook(dependentType, targetHook)) return container; + if (ShouldExecuteHook(rightType, targetHook)) return container; } return null; } /// - public IResourceHookContainer GetResourceHookContainer(ResourceHook hook = ResourceHook.None) where TEntity : class, IIdentifiable + public IResourceHookContainer GetResourceHookContainer(ResourceHook hook = ResourceHook.None) where TResource : class, IIdentifiable { - return (IResourceHookContainer)GetResourceHookContainer(typeof(TEntity), hook); + return (IResourceHookContainer)GetResourceHookContainer(typeof(TResource), hook); } - public IEnumerable LoadDbValues(PrincipalType entityTypeForRepository, IEnumerable entities, ResourceHook hook, params RelationshipAttribute[] relationships) + public IEnumerable LoadDbValues(LeftType entityTypeForRepository, IEnumerable entities, ResourceHook hook, params RelationshipAttribute[] inclusionChain) { - var paths = relationships.Select(p => p.RelationshipPath).ToArray(); var idType = TypeHelper.GetIdentifierType(entityTypeForRepository); var parameterizedGetWhere = GetType() .GetMethod(nameof(GetWhereAndInclude), BindingFlags.NonPublic | BindingFlags.Instance) .MakeGenericMethod(entityTypeForRepository, idType); var casted = ((IEnumerable)entities).Cast(); var ids = casted.Select(e => e.StringId).Cast(idType); - var values = (IEnumerable)parameterizedGetWhere.Invoke(this, new object[] { ids, paths }); + var values = (IEnumerable)parameterizedGetWhere.Invoke(this, new object[] { ids, inclusionChain }); if (values == null) return null; return (IEnumerable)Activator.CreateInstance(typeof(HashSet<>).MakeGenericType(entityTypeForRepository), values.Cast(entityTypeForRepository)); } - public HashSet LoadDbValues(IEnumerable entities, ResourceHook hook, params RelationshipAttribute[] relationships) where TEntity : class, IIdentifiable + public HashSet LoadDbValues(IEnumerable entities, ResourceHook hook, params RelationshipAttribute[] relationships) where TResource : class, IIdentifiable { - var entityType = typeof(TEntity); - var dbValues = LoadDbValues(entityType, entities, hook, relationships)?.Cast(); + var entityType = typeof(TResource); + var dbValues = LoadDbValues(entityType, entities, hook, relationships)?.Cast(); if (dbValues == null) return null; - return new HashSet(dbValues); + return new HashSet(dbValues); } - public bool ShouldLoadDbValues(Type entityType, ResourceHook hook) { var discovery = GetHookDiscovery(entityType); - if (discovery.DatabaseValuesDisabledHooks.Contains(hook)) - { return false; - } - else if (discovery.DatabaseValuesEnabledHooks.Contains(hook)) - { + if (discovery.DatabaseValuesEnabledHooks.Contains(hook)) return true; - } - else - { - return _context.Options.LoadDatabaseValues; - } + return _options.LoaDatabaseValues; } - bool ShouldExecuteHook(DependentType entityType, ResourceHook hook) + bool ShouldExecuteHook(RightType entityType, ResourceHook hook) { var discovery = GetHookDiscovery(entityType); return discovery.ImplementedHooks.Contains(hook); @@ -138,70 +123,66 @@ IHooksDiscovery GetHookDiscovery(Type entityType) { if (!_hookDiscoveries.TryGetValue(entityType, out IHooksDiscovery discovery)) { - discovery = _genericProcessorFactory.GetProcessor(typeof(IHooksDiscovery<>), entityType); + discovery = _genericProcessorFactory.Get(typeof(IHooksDiscovery<>), entityType); _hookDiscoveries[entityType] = discovery; } return discovery; } - IEnumerable GetWhereAndInclude(IEnumerable ids, string[] relationshipPaths) where TEntity : class, IIdentifiable + IEnumerable GetWhereAndInclude(IEnumerable ids, RelationshipAttribute[] inclusionChain) where TResource : class, IIdentifiable { - var repo = GetRepository(); + var repo = GetRepository(); var query = repo.Get().Where(e => ids.Contains(e.Id)); - foreach (var path in relationshipPaths) - { - query = query.Include(path); - } - return query.ToList(); + return repo.Include(query, inclusionChain).ToList(); } - IEntityReadRepository GetRepository() where TEntity : class, IIdentifiable + IResourceReadRepository GetRepository() where TResource : class, IIdentifiable { - return _genericProcessorFactory.GetProcessor>(typeof(IEntityReadRepository<,>), typeof(TEntity), typeof(TId)); + return _genericProcessorFactory.Get>(typeof(IResourceReadRepository<,>), typeof(TResource), typeof(TId)); } public Dictionary LoadImplicitlyAffected( - Dictionary principalEntitiesByRelation, - IEnumerable existingDependentEntities = null) + Dictionary leftEntitiesByRelation, + IEnumerable existingRightEntities = null) { var implicitlyAffected = new Dictionary(); - foreach (var kvp in principalEntitiesByRelation) + foreach (var kvp in leftEntitiesByRelation) { - if (IsHasManyThrough(kvp, out var principals, out var relationship)) continue; + if (IsHasManyThrough(kvp, out var lefts, out var relationship)) continue; // note that we dont't have to check if BeforeImplicitUpdate hook is implemented. If not, it wont ever get here. - var includedPrincipals = LoadDbValues(relationship.PrincipalType, principals, ResourceHook.BeforeImplicitUpdateRelationship, relationship); + var includedLefts = LoadDbValues(relationship.LeftType, lefts, ResourceHook.BeforeImplicitUpdateRelationship, relationship); - foreach (IIdentifiable ip in includedPrincipals) + foreach (IIdentifiable ip in includedLefts) { - IList dbDependentEntityList = null; + IList dbRightEntityList = null; var relationshipValue = relationship.GetValue(ip); if (!(relationshipValue is IEnumerable)) { - dbDependentEntityList = TypeHelper.CreateListFor(relationship.DependentType); - if (relationshipValue != null) dbDependentEntityList.Add(relationshipValue); + dbRightEntityList = TypeHelper.CreateListFor(relationship.RightType); + if (relationshipValue != null) dbRightEntityList.Add(relationshipValue); } else { - dbDependentEntityList = (IList)relationshipValue; + dbRightEntityList = (IList)relationshipValue; } - var dbDependentEntityListCasted = dbDependentEntityList.Cast().ToList(); - if (existingDependentEntities != null) dbDependentEntityListCasted = dbDependentEntityListCasted.Except(existingDependentEntities.Cast(), ResourceHookExecutor.Comparer).ToList(); + var dbRightEntityListCasted = dbRightEntityList.Cast().ToList(); + if (existingRightEntities != null) dbRightEntityListCasted = dbRightEntityListCasted.Except(existingRightEntities.Cast(), _comparer).ToList(); - if (dbDependentEntityListCasted.Any()) + if (dbRightEntityListCasted.Any()) { if (!implicitlyAffected.TryGetValue(relationship, out IEnumerable affected)) { - affected = TypeHelper.CreateListFor(relationship.DependentType); + affected = TypeHelper.CreateListFor(relationship.RightType); implicitlyAffected[relationship] = affected; } - ((IList)affected).AddRange(dbDependentEntityListCasted); + ((IList)affected).AddRange(dbRightEntityListCasted); } } } - return implicitlyAffected.ToDictionary(kvp => kvp.Key, kvp => TypeHelper.CreateHashSetFor(kvp.Key.DependentType, kvp.Value)); + return implicitlyAffected.ToDictionary(kvp => kvp.Key, kvp => TypeHelper.CreateHashSetFor(kvp.Key.RightType, kvp.Value)); } @@ -219,4 +200,4 @@ bool IsHasManyThrough(KeyValuePair kvp, return (kvp.Key is HasManyThroughAttribute); } } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Hooks/Execution/IHookExecutorHelper.cs b/src/JsonApiDotNetCore/Hooks/Execution/IHookExecutorHelper.cs index 98de544f0a..214f1b000d 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/IHookExecutorHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Execution/IHookExecutorHelper.cs @@ -31,13 +31,13 @@ internal interface IHookExecutorHelper /// Also caches the retrieves containers so we don't need to reflectively /// instantiate them multiple times. /// - IResourceHookContainer GetResourceHookContainer(ResourceHook hook = ResourceHook.None) where TEntity : class, IIdentifiable; + IResourceHookContainer GetResourceHookContainer(ResourceHook hook = ResourceHook.None) where TResource : class, IIdentifiable; /// /// Load the implicitly affected entities from the database for a given set of target target entities and involved relationships /// /// The implicitly affected entities by relationship - Dictionary LoadImplicitlyAffected(Dictionary principalEntities, IEnumerable existingDependentEntities = null); + Dictionary LoadImplicitlyAffected(Dictionary leftEntities, IEnumerable existingRightEntities = null); /// /// For a set of entities, loads current values from the database diff --git a/src/JsonApiDotNetCore/Hooks/Execution/RelationshipsDictionary.cs b/src/JsonApiDotNetCore/Hooks/Execution/RelationshipsDictionary.cs index b967e31464..45b6cd0eca 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/RelationshipsDictionary.cs +++ b/src/JsonApiDotNetCore/Hooks/Execution/RelationshipsDictionary.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Reflection; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; @@ -17,52 +16,51 @@ public interface IRelationshipsDictionary { } /// /// An interface that is implemented to expose a relationship dictionary on another class. /// - public interface IByAffectedRelationships : - IRelationshipGetters where TDependentResource : class, IIdentifiable + public interface IByAffectedRelationships : + IRelationshipGetters where TRightResource : class, IIdentifiable { /// /// Gets a dictionary of affected resources grouped by affected relationships. /// - Dictionary> AffectedRelationships { get; } + Dictionary> AffectedRelationships { get; } } /// /// A helper class that provides insights in which relationships have been updated for which entities. /// - public interface IRelationshipsDictionary : - IRelationshipGetters, - IReadOnlyDictionary>, - IRelationshipsDictionary where TDependentResource : class, IIdentifiable + public interface IRelationshipsDictionary : + IRelationshipGetters, + IReadOnlyDictionary>, + IRelationshipsDictionary where TRightResource : class, IIdentifiable { } /// /// A helper class that provides insights in which relationships have been updated for which entities. /// - public interface IRelationshipGetters where TResource : class, IIdentifiable + public interface IRelationshipGetters where TLeftResource : class, IIdentifiable { /// - /// Gets a dictionary of all entities that have an affected relationship to type + /// Gets a dictionary of all entities that have an affected relationship to type /// - Dictionary> GetByRelationship() where TRelatedResource : class, IIdentifiable; + Dictionary> GetByRelationship() where TRightResource : class, IIdentifiable; /// - /// Gets a dictionary of all entities that have an affected relationship to type + /// Gets a dictionary of all entities that have an affected relationship to type /// - Dictionary> GetByRelationship(Type relatedResourceType); - + Dictionary> GetByRelationship(Type relatedResourceType); /// - /// Gets a collection of all the entities for the property within + /// Gets a collection of all the entities for the property within /// has been affected by the request /// - /// - HashSet GetAffected(Expression> NavigationAction); + /// + HashSet GetAffected(Expression> navigationAction); } /// - /// Implementation of IAffectedRelationships{TDependentResource} + /// Implementation of IAffectedRelationships{TRightResource} /// - /// It is practically a ReadOnlyDictionary{RelationshipAttribute, HashSet{TDependentResource}} dictionary - /// with the two helper methods defined on IAffectedRelationships{TDependentResource}. + /// It is practically a ReadOnlyDictionary{RelationshipAttribute, HashSet{TRightResource}} dictionary + /// with the two helper methods defined on IAffectedRelationships{TRightResource}. /// public class RelationshipsDictionary : Dictionary>, @@ -89,7 +87,7 @@ public Dictionary> GetByRelationship public Dictionary> GetByRelationship(Type relatedType) { - return this.Where(p => p.Key.DependentType == relatedType).ToDictionary(p => p.Key, p => p.Value); + return this.Where(p => p.Key.RightType == relatedType).ToDictionary(p => p.Key, p => p.Value); } /// diff --git a/src/JsonApiDotNetCore/Hooks/Execution/ResourcePipelineEnum.cs b/src/JsonApiDotNetCore/Hooks/Execution/ResourcePipelineEnum.cs index 177423ed4f..3f952c8f52 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/ResourcePipelineEnum.cs +++ b/src/JsonApiDotNetCore/Hooks/Execution/ResourcePipelineEnum.cs @@ -14,9 +14,6 @@ public enum ResourcePipeline Post, Patch, PatchRelationship, - Delete, - BulkPost, - BulkPatch, - BulkDelete + Delete } } \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Hooks/IResourceHookContainer.cs b/src/JsonApiDotNetCore/Hooks/IResourceHookContainer.cs index 38d296f8f0..cffc1371c9 100644 --- a/src/JsonApiDotNetCore/Hooks/IResourceHookContainer.cs +++ b/src/JsonApiDotNetCore/Hooks/IResourceHookContainer.cs @@ -12,22 +12,48 @@ public interface IResourceHookContainer { } /// /// Implement this interface to implement business logic hooks on . /// - public interface IResourceHookContainer : IBeforeHooks, IAfterHooks, IOnHooks, IResourceHookContainer where TResource : class, IIdentifiable { } + public interface IResourceHookContainer + : IReadHookContainer, IDeleteHookContainer, ICreateHookContainer, + IUpdateHookContainer, IOnReturnHookContainer, IResourceHookContainer + where TResource : class, IIdentifiable { } /// - /// Wrapper interface for all Before hooks. + /// Read hooks container /// - public interface IBeforeHooks where TResource : class, IIdentifiable + public interface IReadHookContainer where TResource : class, IIdentifiable { /// - /// Implement this hook to run custom logic in the + /// Implement this hook to run custom logic in the + /// layer just before reading entities of type . + /// + /// An enum indicating from where the hook was triggered. + /// Indicates whether the to be queried entities are the main request entities or if they were included + /// The string id of the requested entity, in the case of + void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null); + /// + /// Implement this hook to run custom logic in the + /// layer just after reading entities of type . + /// + /// The unique set of affected entities. + /// An enum indicating from where the hook was triggered. + /// A boolean to indicate whether the entities in this hook execution are the main entities of the request, + /// or if they were included as a relationship + void AfterRead(HashSet entities, ResourcePipeline pipeline, bool isIncluded = false); + } + + /// + /// Create hooks container + /// + public interface ICreateHookContainer where TResource : class, IIdentifiable + { + /// + /// Implement this hook to run custom logic in the /// layer just before creation of entities of type . /// /// For the pipeline, - /// will typically contain one entry. For , - /// can contain multiple entities. + /// will typically contain one entry. /// - /// The returned may be a subset + /// The returned may be a subset /// of , in which case the operation of the /// pipeline will not be executed for the omitted entities. The returned /// set may also contain custom changes of the properties on the entities. @@ -41,25 +67,36 @@ public interface IBeforeHooks where TResource : class, IIdentifiable /// The unique set of affected entities. /// An enum indicating from where the hook was triggered. IEnumerable BeforeCreate(IEntityHashSet entities, ResourcePipeline pipeline); + /// - /// Implement this hook to run custom logic in the - /// layer just before reading entities of type . + /// Implement this hook to run custom logic in the + /// layer just after creation of entities of type . + /// + /// If relationships were created with the created entities, this will + /// be reflected by the corresponding NavigationProperty being set. + /// For each of these relationships, the + /// hook is fired after the execution of this hook. /// + /// The transformed entity set + /// The unique set of affected entities. /// An enum indicating from where the hook was triggered. - /// Indicates whether the to be queried entities are the main request entities or if they were included - /// The string id of the requested entity, in the case of - void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null); + void AfterCreate(HashSet entities, ResourcePipeline pipeline); + } + + /// + /// update hooks container + /// + public interface IUpdateHookContainer where TResource : class, IIdentifiable + { /// - /// Implement this hook to run custom logic in the + /// Implement this hook to run custom logic in the /// layer just before updating entities of type . /// /// For the pipeline, the /// will typically contain one entity. - /// For , this it may contain - /// multiple entities. /// - /// The returned may be a subset - /// of the property in parameter , + /// The returned may be a subset + /// of the property in parameter , /// in which case the operation of the pipeline will not be executed /// for the omitted entities. The returned set may also contain custom /// changes of the properties on the entities. @@ -80,29 +117,7 @@ public interface IBeforeHooks where TResource : class, IIdentifiable IEnumerable BeforeUpdate(IDiffableEntityHashSet entities, ResourcePipeline pipeline); /// - /// Implement this hook to run custom logic in the - /// layer just before deleting entities of type . - /// - /// For the pipeline, - /// will typically contain one entity. - /// For , this it may contain - /// multiple entities. - /// - /// The returned may be a subset - /// of , in which case the operation of the - /// pipeline will not be executed for the omitted entities. - /// - /// If by the deletion of these entities any other entities are affected - /// implicitly by the removal of their relationships (eg - /// in the case of an one-to-one relationship), the - /// hook is fired for these entities. - /// - /// The transformed entity set - /// The unique set of affected entities. - /// An enum indicating from where the hook was triggered. - IEnumerable BeforeDelete(IEntityHashSet entities, ResourcePipeline pipeline); - /// - /// Implement this hook to run custom logic in the + /// Implement this hook to run custom logic in the /// layer just before updating relationships to entities of type . /// /// This hook is fired when a relationship is created to entities of type @@ -111,7 +126,7 @@ public interface IBeforeHooks where TResource : class, IIdentifiable /// and its author relationship was set to an existing Person, this hook will be fired /// for that particular Person. /// - /// The returned may be a subset + /// The returned may be a subset /// of , in which case the operation of the /// pipeline will not be executed for any entity whose id was omitted /// @@ -121,8 +136,30 @@ public interface IBeforeHooks where TResource : class, IIdentifiable /// An enum indicating from where the hook was triggered. /// A helper that groups the entities by the affected relationship IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary entitiesByRelationship, ResourcePipeline pipeline); + /// - /// Implement this hook to run custom logic in the + /// Implement this hook to run custom logic in the + /// layer just after updating entities of type . + /// + /// If relationships were updated with the updated entities, this will + /// be reflected by the corresponding NavigationProperty being set. + /// For each of these relationships, the + /// hook is fired after the execution of this hook. + /// + /// The unique set of affected entities. + /// An enum indicating from where the hook was triggered. + void AfterUpdate(HashSet entities, ResourcePipeline pipeline); + + /// + /// Implement this hook to run custom logic in the layer + /// just after a relationship was updated. + /// + /// Relationship helper. + /// An enum indicating from where the hook was triggered. + void AfterUpdateRelationship(IRelationshipsDictionary entitiesByRelationship, ResourcePipeline pipeline); + + /// + /// Implement this hook to run custom logic in the /// layer just before implicitly updating relationships to entities of type . /// /// This hook is fired when a relationship to entities of type @@ -132,7 +169,7 @@ public interface IBeforeHooks where TResource : class, IIdentifiable /// and by this the relationship to a different Person was implicitly removed, /// this hook will be fired for the latter Person. /// - /// See for information about + /// See for information about /// when this hook is fired. /// /// @@ -143,72 +180,52 @@ public interface IBeforeHooks where TResource : class, IIdentifiable } /// - /// Wrapper interface for all After hooks. + /// Delete hooks container /// - public interface IAfterHooks where TResource : class, IIdentifiable + public interface IDeleteHookContainer where TResource : class, IIdentifiable { /// - /// Implement this hook to run custom logic in the - /// layer just after creation of entities of type . + /// Implement this hook to run custom logic in the + /// layer just before deleting entities of type . /// - /// If relationships were created with the created entities, this will - /// be reflected by the corresponding NavigationProperty being set. - /// For each of these relationships, the - /// hook is fired after the execution of this hook. - /// - /// The transformed entity set - /// The unique set of affected entities. - /// An enum indicating from where the hook was triggered. - void AfterCreate(HashSet entities, ResourcePipeline pipeline); - /// - /// Implement this hook to run custom logic in the - /// layer just after reading entities of type . - /// - /// The unique set of affected entities. - /// An enum indicating from where the hook was triggered. - /// A boolean to indicate whether the entities in this hook execution are the main entities of the request, - /// or if they were included as a relationship - void AfterRead(HashSet entities, ResourcePipeline pipeline, bool isIncluded = false); - /// - /// Implement this hook to run custom logic in the - /// layer just after updating entities of type . + /// For the pipeline, + /// will typically contain one entity. /// - /// If relationships were updated with the updated entities, this will - /// be reflected by the corresponding NavigationProperty being set. - /// For each of these relationships, the - /// hook is fired after the execution of this hook. + /// The returned may be a subset + /// of , in which case the operation of the + /// pipeline will not be executed for the omitted entities. + /// + /// If by the deletion of these entities any other entities are affected + /// implicitly by the removal of their relationships (eg + /// in the case of an one-to-one relationship), the + /// hook is fired for these entities. /// + /// The transformed entity set /// The unique set of affected entities. /// An enum indicating from where the hook was triggered. - void AfterUpdate(HashSet entities, ResourcePipeline pipeline); + IEnumerable BeforeDelete(IEntityHashSet entities, ResourcePipeline pipeline); + /// - /// Implement this hook to run custom logic in the + /// Implement this hook to run custom logic in the /// layer just after deletion of entities of type . /// /// The unique set of affected entities. /// An enum indicating from where the hook was triggered. /// If set to true if the deletion was succeeded in the repository layer. void AfterDelete(HashSet entities, ResourcePipeline pipeline, bool succeeded); - /// - /// Implement this hook to run custom logic in the layer - /// just after a relationship was updated. - /// - /// Relationship helper. - /// An enum indicating from where the hook was triggered. - void AfterUpdateRelationship(IRelationshipsDictionary entitiesByRelationship, ResourcePipeline pipeline); } /// - /// Wrapper interface for all on hooks. + /// On return hook container /// - public interface IOnHooks where TResource : class, IIdentifiable + public interface IOnReturnHookContainer where TResource : class, IIdentifiable { /// /// Implement this hook to transform the result data just before returning /// the entities of type from the - /// layer + /// layer /// - /// The returned may be a subset + /// The returned may be a subset /// of and may contain changes in properties /// of the encapsulated entities. /// diff --git a/src/JsonApiDotNetCore/Hooks/IResourceHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/IResourceHookExecutor.cs index 2d4f1fbdb7..44ab2929bf 100644 --- a/src/JsonApiDotNetCore/Hooks/IResourceHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/IResourceHookExecutor.cs @@ -7,23 +7,20 @@ namespace JsonApiDotNetCore.Hooks /// /// Transient service responsible for executing Resource Hooks as defined /// in . see methods in - /// , and - /// for more information. + /// , and + /// for more information. /// /// Uses for traversal of nested entity data structures. /// Uses for retrieving meta data about hooks, /// fetching database values and performing other recurring internal operations. /// - public interface IResourceHookExecutor : IBeforeExecutor, IAfterExecutor, IOnExecutor { } + public interface IResourceHookExecutor : IReadHookExecutor, IUpdateHookExecutor, ICreateHookExecutor, IDeleteHookExecutor, IOnReturnHookExecutor { } - /// - /// Wrapper interface for all Before execution methods. - /// - public interface IBeforeExecutor + public interface ICreateHookExecutor { /// /// Executes the Before Cycle by firing the appropiate hooks if they are implemented. - /// The returned set will be used in the actual operation in . + /// The returned set will be used in the actual operation in . /// /// Fires the /// hook where T = for values in parameter . @@ -37,41 +34,25 @@ public interface IBeforeExecutor /// The type of the root entities IEnumerable BeforeCreate(IEnumerable entities, ResourcePipeline pipeline) where TResource : class, IIdentifiable; /// - /// Executes the Before Cycle by firing the appropiate hooks if they are implemented. - /// - /// Fires the - /// hook where T = for the requested - /// entities as well as any related relationship. - /// - /// An enum indicating from where the hook was triggered. - /// StringId of the requested entity in the case of - /// . - /// The type of the request entity - void BeforeRead(ResourcePipeline pipeline, string stringId = null) where TResource : class, IIdentifiable; - /// - /// Executes the Before Cycle by firing the appropiate hooks if they are implemented. - /// The returned set will be used in the actual operation in . + /// Executes the After Cycle by firing the appropiate hooks if they are implemented. /// - /// Fires the + /// Fires the /// hook where T = for values in parameter . /// - /// Fires the + /// Fires the /// hook for any related (nested) entity for values within parameter - /// - /// Fires the - /// hook for any entities that are indirectly (implicitly) affected by this operation. - /// Eg: when updating a one-to-one relationship of an entity which already - /// had this relationship populated, then this update will indirectly affect - /// the existing relationship value. /// - /// The transformed set /// Target entities for the Before cycle. /// An enum indicating from where the hook was triggered. /// The type of the root entities - IEnumerable BeforeUpdate(IEnumerable entities, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + void AfterCreate(IEnumerable entities, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + } + + public interface IDeleteHookExecutor + { /// /// Executes the Before Cycle by firing the appropiate hooks if they are implemented. - /// The returned set will be used in the actual operation in . + /// The returned set will be used in the actual operation in . /// /// Fires the /// hook where T = for values in parameter . @@ -86,26 +67,35 @@ public interface IBeforeExecutor /// An enum indicating from where the hook was triggered. /// The type of the root entities IEnumerable BeforeDelete(IEnumerable entities, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + /// + /// Executes the After Cycle by firing the appropiate hooks if they are implemented. + /// + /// Fires the + /// hook where T = for values in parameter . + /// + /// Target entities for the Before cycle. + /// An enum indicating from where the hook was triggered. + /// The type of the root entities + void AfterDelete(IEnumerable entities, ResourcePipeline pipeline, bool succeeded) where TResource : class, IIdentifiable; } /// - /// Wrapper interface for all After execution methods. + /// Wrapper interface for all Before execution methods. /// - public interface IAfterExecutor + public interface IReadHookExecutor { /// - /// Executes the After Cycle by firing the appropiate hooks if they are implemented. - /// - /// Fires the - /// hook where T = for values in parameter . + /// Executes the Before Cycle by firing the appropiate hooks if they are implemented. /// - /// Fires the - /// hook for any related (nested) entity for values within parameter + /// Fires the + /// hook where T = for the requested + /// entities as well as any related relationship. /// - /// Target entities for the Before cycle. /// An enum indicating from where the hook was triggered. - /// The type of the root entities - void AfterCreate(IEnumerable entities, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + /// StringId of the requested entity in the case of + /// . + /// The type of the request entity + void BeforeRead(ResourcePipeline pipeline, string stringId = null) where TResource : class, IIdentifiable; /// /// Executes the After Cycle by firing the appropiate hooks if they are implemented. /// @@ -116,35 +106,53 @@ public interface IAfterExecutor /// An enum indicating from where the hook was triggered. /// The type of the root entities void AfterRead(IEnumerable entities, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + } + + /// + /// Wrapper interface for all After execution methods. + /// + public interface IUpdateHookExecutor + { /// - /// Executes the After Cycle by firing the appropiate hooks if they are implemented. + /// Executes the Before Cycle by firing the appropiate hooks if they are implemented. + /// The returned set will be used in the actual operation in . /// - /// Fires the + /// Fires the /// hook where T = for values in parameter . /// - /// Fires the + /// Fires the /// hook for any related (nested) entity for values within parameter + /// + /// Fires the + /// hook for any entities that are indirectly (implicitly) affected by this operation. + /// Eg: when updating a one-to-one relationship of an entity which already + /// had this relationship populated, then this update will indirectly affect + /// the existing relationship value. /// + /// The transformed set /// Target entities for the Before cycle. /// An enum indicating from where the hook was triggered. /// The type of the root entities - void AfterUpdate(IEnumerable entities, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + IEnumerable BeforeUpdate(IEnumerable entities, ResourcePipeline pipeline) where TResource : class, IIdentifiable; /// /// Executes the After Cycle by firing the appropiate hooks if they are implemented. /// - /// Fires the + /// Fires the /// hook where T = for values in parameter . + /// + /// Fires the + /// hook for any related (nested) entity for values within parameter /// /// Target entities for the Before cycle. /// An enum indicating from where the hook was triggered. /// The type of the root entities - void AfterDelete(IEnumerable entities, ResourcePipeline pipeline, bool succeeded) where TResource : class, IIdentifiable; + void AfterUpdate(IEnumerable entities, ResourcePipeline pipeline) where TResource : class, IIdentifiable; } /// /// Wrapper interface for all On execution methods. /// - public interface IOnExecutor + public interface IOnReturnHookExecutor { /// /// Executes the On Cycle by firing the appropiate hooks if they are implemented. diff --git a/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs index 338fa6ee6b..f8e4dad3ac 100644 --- a/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs @@ -5,52 +5,56 @@ using System.Reflection; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; -using PrincipalType = System.Type; -using DependentType = System.Type; -using JsonApiDotNetCore.Services; +using LeftType = System.Type; +using RightType = System.Type; using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Query; namespace JsonApiDotNetCore.Hooks { /// internal class ResourceHookExecutor : IResourceHookExecutor { - public static readonly IdentifiableComparer Comparer = new IdentifiableComparer(); - internal readonly TraversalHelper _traversalHelper; internal readonly IHookExecutorHelper _executorHelper; - protected readonly IJsonApiContext _context; - private readonly IResourceGraph _graph; - - public ResourceHookExecutor(IHookExecutorHelper helper, IJsonApiContext context, IResourceGraph graph) + private readonly ITraversalHelper _traversalHelper; + private readonly IIncludeService _includeService; + private readonly ITargetedFields _targetedFields; + private readonly IResourceGraph _resourceGraph; + public ResourceHookExecutor( + IHookExecutorHelper executorHelper, + ITraversalHelper traversalHelper, + ITargetedFields targetedFields, + IIncludeService includedRelationships, + IResourceGraph resourceGraph) { - _executorHelper = helper; - _context = context; - _graph = graph; - _traversalHelper = new TraversalHelper(graph, _context); + _executorHelper = executorHelper; + _traversalHelper = traversalHelper; + _targetedFields = targetedFields; + _includeService = includedRelationships; + _resourceGraph = resourceGraph; } /// - public virtual void BeforeRead(ResourcePipeline pipeline, string stringId = null) where TEntity : class, IIdentifiable + public virtual void BeforeRead(ResourcePipeline pipeline, string stringId = null) where TResource : class, IIdentifiable { - var hookContainer = _executorHelper.GetResourceHookContainer(ResourceHook.BeforeRead); + var hookContainer = _executorHelper.GetResourceHookContainer(ResourceHook.BeforeRead); hookContainer?.BeforeRead(pipeline, false, stringId); - var contextEntity = _graph.GetContextEntity(typeof(TEntity)); - var calledContainers = new List() { typeof(TEntity) }; - foreach (var relationshipPath in _context.IncludedRelationships) - { - RecursiveBeforeRead(contextEntity, relationshipPath.Split('.').ToList(), pipeline, calledContainers); - } + var calledContainers = new List() { typeof(TResource) }; + foreach (var chain in _includeService.Get()) + RecursiveBeforeRead(chain, pipeline, calledContainers); } /// - public virtual IEnumerable BeforeUpdate(IEnumerable entities, ResourcePipeline pipeline) where TEntity : class, IIdentifiable + public virtual IEnumerable BeforeUpdate(IEnumerable entities, ResourcePipeline pipeline) where TResource : class, IIdentifiable { if (GetHook(ResourceHook.BeforeUpdate, entities, out var container, out var node)) { var relationships = node.RelationshipsToNextLayer.Select(p => p.Attribute).ToArray(); - var dbValues = LoadDbValues(typeof(TEntity), (IEnumerable)node.UniqueEntities, ResourceHook.BeforeUpdate, relationships); - var diff = new DiffableEntityHashSet(node.UniqueEntities, dbValues, node.PrincipalsToNextLayer(), _context); - IEnumerable updated = container.BeforeUpdate(diff, pipeline); + var dbValues = LoadDbValues(typeof(TResource), (IEnumerable)node.UniqueEntities, ResourceHook.BeforeUpdate, relationships); + var diff = new DiffableEntityHashSet(node.UniqueEntities, dbValues, node.LeftsToNextLayer(), _targetedFields); + IEnumerable updated = container.BeforeUpdate(diff, pipeline); node.UpdateUnique(updated); node.Reassign(entities); } @@ -60,12 +64,12 @@ public virtual IEnumerable BeforeUpdate(IEnumerable e } /// - public virtual IEnumerable BeforeCreate(IEnumerable entities, ResourcePipeline pipeline) where TEntity : class, IIdentifiable + public virtual IEnumerable BeforeCreate(IEnumerable entities, ResourcePipeline pipeline) where TResource : class, IIdentifiable { if (GetHook(ResourceHook.BeforeCreate, entities, out var container, out var node)) { - var affected = new EntityHashSet((HashSet)node.UniqueEntities, node.PrincipalsToNextLayer()); - IEnumerable updated = container.BeforeCreate(affected, pipeline); + var affected = new EntityHashSet((HashSet)node.UniqueEntities, node.LeftsToNextLayer()); + IEnumerable updated = container.BeforeCreate(affected, pipeline); node.UpdateUnique(updated); node.Reassign(entities); } @@ -74,15 +78,15 @@ public virtual IEnumerable BeforeCreate(IEnumerable e } /// - public virtual IEnumerable BeforeDelete(IEnumerable entities, ResourcePipeline pipeline) where TEntity : class, IIdentifiable + public virtual IEnumerable BeforeDelete(IEnumerable entities, ResourcePipeline pipeline) where TResource : class, IIdentifiable { if (GetHook(ResourceHook.BeforeDelete, entities, out var container, out var node)) { var relationships = node.RelationshipsToNextLayer.Select(p => p.Attribute).ToArray(); - var targetEntities = LoadDbValues(typeof(TEntity), (IEnumerable)node.UniqueEntities, ResourceHook.BeforeDelete, relationships) ?? node.UniqueEntities; - var affected = new EntityHashSet(targetEntities, node.PrincipalsToNextLayer()); + var targetEntities = LoadDbValues(typeof(TResource), (IEnumerable)node.UniqueEntities, ResourceHook.BeforeDelete, relationships) ?? node.UniqueEntities; + var affected = new EntityHashSet(targetEntities, node.LeftsToNextLayer()); - IEnumerable updated = container.BeforeDelete(affected, pipeline); + IEnumerable updated = container.BeforeDelete(affected, pipeline); node.UpdateUnique(updated); node.Reassign(entities); } @@ -91,21 +95,21 @@ public virtual IEnumerable BeforeDelete(IEnumerable e /// Here we're loading all relations onto the to-be-deleted article /// if for that relation the BeforeImplicitUpdateHook is implemented, /// and this hook is then executed - foreach (var entry in node.PrincipalsToNextLayerByRelationships()) + foreach (var entry in node.LeftsToNextLayerByRelationships()) { - var dependentType = entry.Key; + var rightType = entry.Key; var implicitTargets = entry.Value; - FireForAffectedImplicits(dependentType, implicitTargets, pipeline); + FireForAffectedImplicits(rightType, implicitTargets, pipeline); } return entities; } /// - public virtual IEnumerable OnReturn(IEnumerable entities, ResourcePipeline pipeline) where TEntity : class, IIdentifiable + public virtual IEnumerable OnReturn(IEnumerable entities, ResourcePipeline pipeline) where TResource : class, IIdentifiable { if (GetHook(ResourceHook.OnReturn, entities, out var container, out var node) && pipeline != ResourcePipeline.GetRelationship) { - IEnumerable updated = container.OnReturn((HashSet)node.UniqueEntities, pipeline); + IEnumerable updated = container.OnReturn((HashSet)node.UniqueEntities, pipeline); ValidateHookResponse(updated); node.UpdateUnique(updated); node.Reassign(entities); @@ -121,11 +125,11 @@ public virtual IEnumerable OnReturn(IEnumerable entit } /// - public virtual void AfterRead(IEnumerable entities, ResourcePipeline pipeline) where TEntity : class, IIdentifiable + public virtual void AfterRead(IEnumerable entities, ResourcePipeline pipeline) where TResource : class, IIdentifiable { if (GetHook(ResourceHook.AfterRead, entities, out var container, out var node)) { - container.AfterRead((HashSet)node.UniqueEntities, pipeline); + container.AfterRead((HashSet)node.UniqueEntities, pipeline); } Traverse(_traversalHelper.CreateNextLayer(node), ResourceHook.AfterRead, (nextContainer, nextNode) => @@ -135,11 +139,11 @@ public virtual void AfterRead(IEnumerable entities, ResourcePi } /// - public virtual void AfterCreate(IEnumerable entities, ResourcePipeline pipeline) where TEntity : class, IIdentifiable + public virtual void AfterCreate(IEnumerable entities, ResourcePipeline pipeline) where TResource : class, IIdentifiable { if (GetHook(ResourceHook.AfterCreate, entities, out var container, out var node)) { - container.AfterCreate((HashSet)node.UniqueEntities, pipeline); + container.AfterCreate((HashSet)node.UniqueEntities, pipeline); } Traverse(_traversalHelper.CreateNextLayer(node), @@ -148,11 +152,11 @@ public virtual void AfterCreate(IEnumerable entities, Resource } /// - public virtual void AfterUpdate(IEnumerable entities, ResourcePipeline pipeline) where TEntity : class, IIdentifiable + public virtual void AfterUpdate(IEnumerable entities, ResourcePipeline pipeline) where TResource : class, IIdentifiable { if (GetHook(ResourceHook.AfterUpdate, entities, out var container, out var node)) { - container.AfterUpdate((HashSet)node.UniqueEntities, pipeline); + container.AfterUpdate((HashSet)node.UniqueEntities, pipeline); } Traverse(_traversalHelper.CreateNextLayer(node), @@ -161,40 +165,40 @@ public virtual void AfterUpdate(IEnumerable entities, Resource } /// - public virtual void AfterDelete(IEnumerable entities, ResourcePipeline pipeline, bool succeeded) where TEntity : class, IIdentifiable + public virtual void AfterDelete(IEnumerable entities, ResourcePipeline pipeline, bool succeeded) where TResource : class, IIdentifiable { if (GetHook(ResourceHook.AfterDelete, entities, out var container, out var node)) { - container.AfterDelete((HashSet)node.UniqueEntities, pipeline, succeeded); + container.AfterDelete((HashSet)node.UniqueEntities, pipeline, succeeded); } } /// /// For a given target and for a given type - /// , gets the hook container if the target + /// , gets the hook container if the target /// hook was implemented and should be executed. /// /// Along the way, creates a traversable node from the root entity set. /// /// true, if hook was implemented, false otherwise. - bool GetHook(ResourceHook target, IEnumerable entities, - out IResourceHookContainer container, - out RootNode node) where TEntity : class, IIdentifiable + bool GetHook(ResourceHook target, IEnumerable entities, + out IResourceHookContainer container, + out RootNode node) where TResource : class, IIdentifiable { node = _traversalHelper.CreateRootNode(entities); - container = _executorHelper.GetResourceHookContainer(target); + container = _executorHelper.GetResourceHookContainer(target); return container != null; } /// - /// Traverses the nodes in a . + /// Traverses the nodes in a . /// - void Traverse(EntityChildLayer currentLayer, ResourceHook target, Action action) + void Traverse(NodeLayer currentLayer, ResourceHook target, Action action) { if (!currentLayer.AnyEntities()) return; - foreach (IEntityNode node in currentLayer) + foreach (INode node in currentLayer) { - var entityType = node.EntityType; + var entityType = node.ResourceType; var hookContainer = _executorHelper.GetResourceHookContainer(entityType, target); if (hookContainer == null) continue; action(hookContainer, node); @@ -208,31 +212,19 @@ void Traverse(EntityChildLayer currentLayer, ResourceHook target, Action - void RecursiveBeforeRead(ContextEntity contextEntity, List relationshipChain, ResourcePipeline pipeline, List calledContainers) + void RecursiveBeforeRead(List relationshipChain, ResourcePipeline pipeline, List calledContainers) { - var target = relationshipChain.First(); - var relationship = contextEntity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == target); - if (relationship == null) - { - throw new JsonApiException(400, $"Invalid relationship {target} on {contextEntity.EntityName}", - $"{contextEntity.EntityName} does not have a relationship named {target}"); - } - - if (!calledContainers.Contains(relationship.DependentType)) + var relationship = relationshipChain.First(); + if (!calledContainers.Contains(relationship.RightType)) { - calledContainers.Add(relationship.DependentType); - var container = _executorHelper.GetResourceHookContainer(relationship.DependentType, ResourceHook.BeforeRead); + calledContainers.Add(relationship.RightType); + var container = _executorHelper.GetResourceHookContainer(relationship.RightType, ResourceHook.BeforeRead); if (container != null) - { CallHook(container, ResourceHook.BeforeRead, new object[] { pipeline, true, null }); - } } relationshipChain.RemoveAt(0); if (relationshipChain.Any()) - { - - RecursiveBeforeRead(_graph.GetContextEntity(relationship.DependentType), relationshipChain, pipeline, calledContainers); - } + RecursiveBeforeRead(relationshipChain, pipeline, calledContainers); } /// @@ -245,13 +237,13 @@ void RecursiveBeforeRead(ContextEntity contextEntity, List relationshipC /// First the BeforeUpdateRelationship should be for owner1, then the /// BeforeImplicitUpdateRelationship hook should be fired for /// owner2, and lastely the BeforeImplicitUpdateRelationship for article2. - void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, EntityChildLayer layer) + void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, NodeLayer layer) { - foreach (IEntityNode node in layer) + foreach (INode node in layer) { - var nestedHookcontainer = _executorHelper.GetResourceHookContainer(node.EntityType, ResourceHook.BeforeUpdateRelationship); + var nestedHookcontainer = _executorHelper.GetResourceHookContainer(node.ResourceType, ResourceHook.BeforeUpdateRelationship); IEnumerable uniqueEntities = node.UniqueEntities; - DependentType entityType = node.EntityType; + RightType entityType = node.ResourceType; Dictionary currenEntitiesGrouped; Dictionary currentEntitiesGroupedInverse; @@ -270,7 +262,7 @@ void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, EntityChildLayer lay /// we want want inverse relationship attribute: /// we now have the one pointing from article -> person, ] /// but we require the the one that points from person -> article - currenEntitiesGrouped = node.RelationshipsFromPreviousLayer.GetDependentEntities(); + currenEntitiesGrouped = node.RelationshipsFromPreviousLayer.GetRightEntities(); currentEntitiesGroupedInverse = ReplaceKeysWithInverseRelationships(currenEntitiesGrouped); var resourcesByRelationship = CreateRelationshipHelper(entityType, currentEntitiesGroupedInverse, dbValues); @@ -289,22 +281,22 @@ void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, EntityChildLayer lay /// To fire a hook for owner_old, we need to first get a reference to it. /// For this, we need to query the database for the HasOneAttribute:owner /// relationship of article1, which is referred to as the - /// principal side of the HasOneAttribute:owner relationship. - var principalEntities = node.RelationshipsFromPreviousLayer.GetPrincipalEntities(); - if (principalEntities.Any()) + /// left side of the HasOneAttribute:owner relationship. + var leftEntities = node.RelationshipsFromPreviousLayer.GetLeftEntities(); + if (leftEntities.Any()) { /// owner_old is loaded, which is an "implicitly affected entity" - FireForAffectedImplicits(entityType, principalEntities, pipeline, uniqueEntities); + FireForAffectedImplicits(entityType, leftEntities, pipeline, uniqueEntities); } } /// Fire the BeforeImplicitUpdateRelationship hook for article2 /// For this, we need to query the database for the current owner /// relationship value of owner_new. - currenEntitiesGrouped = node.RelationshipsFromPreviousLayer.GetDependentEntities(); + currenEntitiesGrouped = node.RelationshipsFromPreviousLayer.GetRightEntities(); if (currenEntitiesGrouped.Any()) { - /// dependentEntities is grouped by relationships from previous + /// rightEntities is grouped by relationships from previous /// layer, ie { HasOneAttribute:owner => owner_new }. But /// to load article2 onto owner_new, we need to have the /// RelationshipAttribute from owner to article, which is the @@ -313,9 +305,9 @@ void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, EntityChildLayer lay /// Note that currently in the JADNC implementation of hooks, /// the root layer is ALWAYS homogenous, so we safely assume /// that for every relationship to the previous layer, the - /// principal type is the same. - PrincipalType principalEntityType = currenEntitiesGrouped.First().Key.PrincipalType; - FireForAffectedImplicits(principalEntityType, currentEntitiesGroupedInverse, pipeline); + /// left type is the same. + LeftType leftType = currenEntitiesGrouped.First().Key.LeftType; + FireForAffectedImplicits(leftType, currentEntitiesGroupedInverse, pipeline); } } } @@ -332,7 +324,7 @@ Dictionary ReplaceKeysWithInverseRelationshi /// If it isn't, JADNC currently knows nothing about this relationship pointing back, and it /// currently cannot fire hooks for entities resolved through inverse relationships. var inversableRelationshipAttributes = entitiesByRelationship.Where(kvp => kvp.Key.InverseNavigation != null); - return inversableRelationshipAttributes.ToDictionary(kvp => _graph.GetInverseRelationship(kvp.Key), kvp => kvp.Value); + return inversableRelationshipAttributes.ToDictionary(kvp => _resourceGraph.GetInverse(kvp.Key), kvp => kvp.Value); } /// @@ -345,7 +337,7 @@ void FireForAffectedImplicits(Type entityTypeToInclude, Dictionary _graph.GetInverseRelationship(kvp.Key), kvp => kvp.Value); + var inverse = implicitAffected.ToDictionary(kvp => _resourceGraph.GetInverse(kvp.Key), kvp => kvp.Value); var resourcesByRelationship = CreateRelationshipHelper(entityTypeToInclude, inverse); CallHook(container, ResourceHook.BeforeImplicitUpdateRelationship, new object[] { resourcesByRelationship, pipeline, }); } @@ -397,7 +389,7 @@ object ThrowJsonApiExceptionOnError(Func action) /// If are included, the values of the entries in need to be replaced with these values. /// /// The relationship helper. - IRelationshipsDictionary CreateRelationshipHelper(DependentType entityType, Dictionary prevLayerRelationships, IEnumerable dbValues = null) + IRelationshipsDictionary CreateRelationshipHelper(RightType entityType, Dictionary prevLayerRelationships, IEnumerable dbValues = null) { if (dbValues != null) prevLayerRelationships = ReplaceWithDbValues(prevLayerRelationships, dbValues.Cast()); return (IRelationshipsDictionary)TypeHelper.CreateInstanceOfOpenType(typeof(RelationshipsDictionary<>), entityType, true, prevLayerRelationships); @@ -411,8 +403,8 @@ Dictionary ReplaceWithDbValues(Dictionary().Select(entity => dbValues.Single(dbEntity => dbEntity.StringId == entity.StringId)).Cast(key.PrincipalType); - prevLayerRelationships[key] = TypeHelper.CreateHashSetFor(key.PrincipalType, replaced); + var replaced = prevLayerRelationships[key].Cast().Select(entity => dbValues.Single(dbEntity => dbEntity.StringId == entity.StringId)).Cast(key.LeftType); + prevLayerRelationships[key] = TypeHelper.CreateHashSetFor(key.LeftType, replaced); } return prevLayerRelationships; } @@ -447,15 +439,15 @@ IEnumerable LoadDbValues(Type entityType, IEnumerable uniqueEntities, ResourceHo /// /// Fires the AfterUpdateRelationship hook /// - void FireAfterUpdateRelationship(IResourceHookContainer container, IEntityNode node, ResourcePipeline pipeline) + void FireAfterUpdateRelationship(IResourceHookContainer container, INode node, ResourcePipeline pipeline) { - Dictionary currenEntitiesGrouped = node.RelationshipsFromPreviousLayer.GetDependentEntities(); + Dictionary currenEntitiesGrouped = node.RelationshipsFromPreviousLayer.GetRightEntities(); /// the relationships attributes in currenEntitiesGrouped will be pointing from a /// resource in the previouslayer to a resource in the current (nested) layer. /// For the nested hook we need to replace these attributes with their inverse. /// See the FireNestedBeforeUpdateHooks method for a more detailed example. - var resourcesByRelationship = CreateRelationshipHelper(node.EntityType, ReplaceKeysWithInverseRelationships(currenEntitiesGrouped)); + var resourcesByRelationship = CreateRelationshipHelper(node.ResourceType, ReplaceKeysWithInverseRelationships(currenEntitiesGrouped)); CallHook(container, ResourceHook.AfterUpdateRelationship, new object[] { resourcesByRelationship, pipeline }); } diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/ChildNode.cs b/src/JsonApiDotNetCore/Hooks/Traversal/ChildNode.cs index 443ee7daf1..00fec56681 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/ChildNode.cs +++ b/src/JsonApiDotNetCore/Hooks/Traversal/ChildNode.cs @@ -1,17 +1,22 @@ -using System.Collections; +using System.Collections; using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; -using DependentType = System.Type; +using RightType = System.Type; namespace JsonApiDotNetCore.Hooks { - /// - internal class ChildNode : IEntityNode where TEntity : class, IIdentifiable + /// + /// Child node in the tree + /// + /// + internal class ChildNode : INode where TResource : class, IIdentifiable { + private readonly IdentifiableComparer _comparer = new IdentifiableComparer(); /// - public DependentType EntityType { get; private set; } + public RightType ResourceType { get; private set; } /// public RelationshipProxy[] RelationshipsToNextLayer { get; set; } /// @@ -19,7 +24,7 @@ public IEnumerable UniqueEntities { get { - return new HashSet(_relationshipsFromPreviousLayer.SelectMany(rfpl => rfpl.DependentEntities)); + return new HashSet(_relationshipsFromPreviousLayer.SelectMany(rfpl => rfpl.RightEntities)); } } @@ -32,11 +37,11 @@ public IRelationshipsFromPreviousLayer RelationshipsFromPreviousLayer } } - private readonly RelationshipsFromPreviousLayer _relationshipsFromPreviousLayer; + private readonly RelationshipsFromPreviousLayer _relationshipsFromPreviousLayer; - public ChildNode(RelationshipProxy[] nextLayerRelationships, RelationshipsFromPreviousLayer prevLayerRelationships) + public ChildNode(RelationshipProxy[] nextLayerRelationships, RelationshipsFromPreviousLayer prevLayerRelationships) { - EntityType = typeof(TEntity); + ResourceType = typeof(TResource); RelationshipsToNextLayer = nextLayerRelationships; _relationshipsFromPreviousLayer = prevLayerRelationships; } @@ -44,42 +49,43 @@ public ChildNode(RelationshipProxy[] nextLayerRelationships, RelationshipsFromPr /// public void UpdateUnique(IEnumerable updated) { - List casted = updated.Cast().ToList(); + List casted = updated.Cast().ToList(); foreach (var rpfl in _relationshipsFromPreviousLayer) { - rpfl.DependentEntities = new HashSet(rpfl.DependentEntities.Intersect(casted, ResourceHookExecutor.Comparer).Cast()); + rpfl.RightEntities = new HashSet(rpfl.RightEntities.Intersect(casted, _comparer).Cast()); } } - /// + /// + /// Reassignment is done according to provided relationships + /// + /// public void Reassign(IEnumerable updated = null) { - var unique = (HashSet)UniqueEntities; + var unique = (HashSet)UniqueEntities; foreach (var rfpl in _relationshipsFromPreviousLayer) { var proxy = rfpl.Proxy; - var principalEntities = rfpl.PrincipalEntities; + var leftEntities = rfpl.LeftEntities; - foreach (IIdentifiable principal in principalEntities) + foreach (IIdentifiable left in leftEntities) { - var currentValue = proxy.GetValue(principal); + var currentValue = proxy.GetValue(left); if (currentValue is IEnumerable relationshipCollection) { - var newValue = relationshipCollection.Intersect(unique, ResourceHookExecutor.Comparer).Cast(proxy.DependentType); - proxy.SetValue(principal, newValue); + var newValue = relationshipCollection.Intersect(unique, _comparer).Cast(proxy.RightType); + proxy.SetValue(left, newValue); } else if (currentValue is IIdentifiable relationshipSingle) { - if (!unique.Intersect(new HashSet() { relationshipSingle }, ResourceHookExecutor.Comparer).Any()) + if (!unique.Intersect(new HashSet() { relationshipSingle }, _comparer).Any()) { - proxy.SetValue(principal, null); + proxy.SetValue(left, null); } } } } } } - - } diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/IEntityNode.cs b/src/JsonApiDotNetCore/Hooks/Traversal/IEntityNode.cs index 159b373ef5..2519449325 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/IEntityNode.cs +++ b/src/JsonApiDotNetCore/Hooks/Traversal/IEntityNode.cs @@ -1,14 +1,17 @@ -using System.Collections; -using DependentType = System.Type; +using System.Collections; +using RightType = System.Type; namespace JsonApiDotNetCore.Hooks { - internal interface IEntityNode + /// + /// This is the interface that nodes need to inherit from + /// + internal interface INode { /// /// Each node representes the entities of a given type throughout a particular layer. /// - DependentType EntityType { get; } + RightType ResourceType { get; } /// /// The unique set of entities in this node. Note that these are all of the same type. /// @@ -35,5 +38,4 @@ internal interface IEntityNode /// Updated. void UpdateUnique(IEnumerable updated); } - } diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/ITraversalHelper.cs b/src/JsonApiDotNetCore/Hooks/Traversal/ITraversalHelper.cs new file mode 100644 index 0000000000..2a93cbb4b0 --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Traversal/ITraversalHelper.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Hooks +{ + internal interface ITraversalHelper + { + /// + /// Crates the next layer + /// + /// + /// + NodeLayer CreateNextLayer(INode node); + /// + /// Creates the next layer based on the nodes provided + /// + /// + /// + NodeLayer CreateNextLayer(IEnumerable nodes); + /// + /// Creates a root node for breadth-first-traversal (BFS). Note that typically, in + /// JADNC, the root layer will be homogeneous. Also, because it is the first layer, + /// there can be no relationships to previous layers, only to next layers. + /// + /// The root node. + /// Root entities. + /// The 1st type parameter. + RootNode CreateRootNode(IEnumerable rootEntities) where TResource : class, IIdentifiable; + } +} diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/RelationshipGroup.cs b/src/JsonApiDotNetCore/Hooks/Traversal/RelationshipGroup.cs index 1ed52419c7..6f34f1fb7b 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/RelationshipGroup.cs +++ b/src/JsonApiDotNetCore/Hooks/Traversal/RelationshipGroup.cs @@ -6,19 +6,19 @@ namespace JsonApiDotNetCore.Hooks internal interface IRelationshipGroup { RelationshipProxy Proxy { get; } - HashSet PrincipalEntities { get; } + HashSet LeftEntities { get; } } - internal class RelationshipGroup : IRelationshipGroup where TDependent : class, IIdentifiable + internal class RelationshipGroup : IRelationshipGroup where TRight : class, IIdentifiable { public RelationshipProxy Proxy { get; } - public HashSet PrincipalEntities { get; } - public HashSet DependentEntities { get; internal set; } - public RelationshipGroup(RelationshipProxy proxy, HashSet principalEntities, HashSet dependentEntities) + public HashSet LeftEntities { get; } + public HashSet RightEntities { get; internal set; } + public RelationshipGroup(RelationshipProxy proxy, HashSet leftEntities, HashSet rightEntities) { Proxy = proxy; - PrincipalEntities = principalEntities; - DependentEntities = dependentEntities; + LeftEntities = leftEntities; + RightEntities = rightEntities; } } } \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/RelationshipProxy.cs b/src/JsonApiDotNetCore/Hooks/Traversal/RelationshipProxy.cs index 6e5d90ecc8..427dbaf2eb 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/RelationshipProxy.cs +++ b/src/JsonApiDotNetCore/Hooks/Traversal/RelationshipProxy.cs @@ -16,8 +16,6 @@ namespace JsonApiDotNetCore.Hooks /// (eg ArticleTags) is identifiable (in which case we will traverse through /// it and fire hooks for it, if defined) or not (in which case we skip /// ArticleTags and go directly to Tags. - /// - /// TODO: We can consider moving fields like DependentType and PrincipalType /// public class RelationshipProxy { @@ -30,20 +28,20 @@ public class RelationshipProxy /// For HasManyThrough it is either the ThroughProperty (when the jointable is /// Identifiable) or it is the righthand side (when the jointable is not identifiable) /// - public Type DependentType { get; private set; } - public Type PrincipalType { get { return Attribute.PrincipalType; } } + public Type RightType { get; private set; } + public Type LeftType { get { return Attribute.LeftType; } } public bool IsContextRelation { get; private set; } public RelationshipAttribute Attribute { get; set; } public RelationshipProxy(RelationshipAttribute attr, Type relatedType, bool isContextRelation) { - DependentType = relatedType; + RightType = relatedType; Attribute = attr; IsContextRelation = isContextRelation; if (attr is HasManyThroughAttribute throughAttr) { _isHasManyThrough = true; - _skipJoinTable |= DependentType != throughAttr.ThroughType; + _skipJoinTable |= RightType != throughAttr.ThroughType; } } @@ -63,22 +61,17 @@ public object GetValue(IIdentifiable entity) { return throughAttr.ThroughProperty.GetValue(entity); } - else - { - var collection = new List(); - var joinEntities = (IList)throughAttr.ThroughProperty.GetValue(entity); - if (joinEntities == null) return null; - - foreach (var joinEntity in joinEntities) - { - var rightEntity = (IIdentifiable)throughAttr.RightProperty.GetValue(joinEntity); - if (rightEntity == null) continue; - collection.Add(rightEntity); - } - return collection; + var collection = new List(); + var joinEntities = (IList)throughAttr.ThroughProperty.GetValue(entity); + if (joinEntities == null) return null; + foreach (var joinEntity in joinEntities) + { + var rightEntity = (IIdentifiable)throughAttr.RightProperty.GetValue(joinEntity); + if (rightEntity == null) continue; + collection.Add(rightEntity); } - + return collection; } return Attribute.GetValue(entity); } @@ -97,27 +90,24 @@ public void SetValue(IIdentifiable entity, object value) if (!_skipJoinTable) { var list = (IEnumerable)value; - ((HasManyThroughAttribute)Attribute).ThroughProperty.SetValue(entity, list.Cast(DependentType)); + ((HasManyThroughAttribute)Attribute).ThroughProperty.SetValue(entity, list.Cast(RightType)); return; } - else + var throughAttr = (HasManyThroughAttribute)Attribute; + var joinEntities = (IEnumerable)throughAttr.ThroughProperty.GetValue(entity); + + var filteredList = new List(); + var rightEntities = ((IEnumerable)value).Cast(RightType); + foreach (var je in joinEntities) { - var throughAttr = (HasManyThroughAttribute)Attribute; - var joinEntities = (IEnumerable)throughAttr.ThroughProperty.GetValue(entity); - var filteredList = new List(); - var rightEntities = ((IEnumerable)value).Cast(DependentType); - foreach (var je in joinEntities) + if (((IList)rightEntities).Contains(throughAttr.RightProperty.GetValue(je))) { - - if (((IList)rightEntities).Contains(throughAttr.RightProperty.GetValue(je))) - { - filteredList.Add(je); - } + filteredList.Add(je); } - throughAttr.ThroughProperty.SetValue(entity, filteredList.Cast(throughAttr.ThroughType)); - return; } + throughAttr.ThroughProperty.SetValue(entity, filteredList.Cast(throughAttr.ThroughType)); + return; } Attribute.SetValue(entity, value); } diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/RelationshipsFromPreviousLayer.cs b/src/JsonApiDotNetCore/Hooks/Traversal/RelationshipsFromPreviousLayer.cs index cb4185d1fd..2068fa8652 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/RelationshipsFromPreviousLayer.cs +++ b/src/JsonApiDotNetCore/Hooks/Traversal/RelationshipsFromPreviousLayer.cs @@ -13,37 +13,39 @@ internal interface IRelationshipsFromPreviousLayer /// /// Grouped by relationship to the previous layer, gets all the entities of the current layer /// - /// The dependent entities. - Dictionary GetDependentEntities(); + /// The right side entities. + Dictionary GetRightEntities(); /// /// Grouped by relationship to the previous layer, gets all the entities of the previous layer /// - /// The dependent entities. - Dictionary GetPrincipalEntities(); + /// The right side entities. + Dictionary GetLeftEntities(); } - internal class RelationshipsFromPreviousLayer : IRelationshipsFromPreviousLayer, IEnumerable> where TDependent : class, IIdentifiable + internal class RelationshipsFromPreviousLayer : IRelationshipsFromPreviousLayer, IEnumerable> where TRightResource : class, IIdentifiable { - readonly IEnumerable> _collection; + readonly IEnumerable> _collection; - public RelationshipsFromPreviousLayer(IEnumerable> collection) + public RelationshipsFromPreviousLayer(IEnumerable> collection) { _collection = collection; } - public Dictionary GetDependentEntities() + /// + public Dictionary GetRightEntities() { - return _collection.ToDictionary(rg => rg.Proxy.Attribute, rg => (IEnumerable)rg.DependentEntities); + return _collection.ToDictionary(rg => rg.Proxy.Attribute, rg => (IEnumerable)rg.RightEntities); } - public Dictionary GetPrincipalEntities() + /// + public Dictionary GetLeftEntities() { - return _collection.ToDictionary(rg => rg.Proxy.Attribute, rg => (IEnumerable)rg.PrincipalEntities); + return _collection.ToDictionary(rg => rg.Proxy.Attribute, rg => (IEnumerable)rg.LeftEntities); } - public IEnumerator> GetEnumerator() + public IEnumerator> GetEnumerator() { - return _collection.Cast>().GetEnumerator(); + return _collection.Cast>().GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/RootNode.cs b/src/JsonApiDotNetCore/Hooks/Traversal/RootNode.cs index 7783d041e1..23fc32c8bb 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/RootNode.cs +++ b/src/JsonApiDotNetCore/Hooks/Traversal/RootNode.cs @@ -1,35 +1,36 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; +using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Hooks { - /// /// The root node class of the breadth-first-traversal of entity data structures /// as performed by the /// - internal class RootNode : IEntityNode where TEntity : class, IIdentifiable + internal class RootNode : INode where TResource : class, IIdentifiable { + private readonly IdentifiableComparer _comparer = new IdentifiableComparer(); private readonly RelationshipProxy[] _allRelationshipsToNextLayer; - private HashSet _uniqueEntities; - public Type EntityType { get; internal set; } + private HashSet _uniqueEntities; + public Type ResourceType { get; internal set; } public IEnumerable UniqueEntities { get { return _uniqueEntities; } } public RelationshipProxy[] RelationshipsToNextLayer { get; } - public Dictionary> PrincipalsToNextLayerByRelationships() + public Dictionary> LeftsToNextLayerByRelationships() { return _allRelationshipsToNextLayer - .GroupBy(proxy => proxy.DependentType) + .GroupBy(proxy => proxy.RightType) .ToDictionary(gdc => gdc.Key, gdc => gdc.ToDictionary(p => p.Attribute, p => UniqueEntities)); } /// /// The current layer entities grouped by affected relationship to the next layer /// - public Dictionary PrincipalsToNextLayer() + public Dictionary LeftsToNextLayer() { return RelationshipsToNextLayer.ToDictionary(p => p.Attribute, p => UniqueEntities); } @@ -39,10 +40,10 @@ public Dictionary PrincipalsToNextLayer() /// public IRelationshipsFromPreviousLayer RelationshipsFromPreviousLayer { get { return null; } } - public RootNode(IEnumerable uniqueEntities, RelationshipProxy[] poplatedRelationships, RelationshipProxy[] allRelationships) + public RootNode(IEnumerable uniqueEntities, RelationshipProxy[] poplatedRelationships, RelationshipProxy[] allRelationships) { - EntityType = typeof(TEntity); - _uniqueEntities = new HashSet(uniqueEntities); + ResourceType = typeof(TResource); + _uniqueEntities = new HashSet(uniqueEntities); RelationshipsToNextLayer = poplatedRelationships; _allRelationshipsToNextLayer = allRelationships; } @@ -53,15 +54,15 @@ public RootNode(IEnumerable uniqueEntities, RelationshipProxy[] poplate /// Updated. public void UpdateUnique(IEnumerable updated) { - var casted = updated.Cast().ToList(); - var intersected = _uniqueEntities.Intersect(casted, ResourceHookExecutor.Comparer).Cast(); - _uniqueEntities = new HashSet(intersected); + var casted = updated.Cast().ToList(); + var intersected = _uniqueEntities.Intersect(casted, _comparer).Cast(); + _uniqueEntities = new HashSet(intersected); } public void Reassign(IEnumerable source = null) { var ids = _uniqueEntities.Select(ue => ue.StringId); - ((List)source).RemoveAll(se => !ids.Contains(se.StringId)); + ((List)source).RemoveAll(se => !ids.Contains(se.StringId)); } } diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs b/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs index 5061cce9bf..1b8fb9c935 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs @@ -1,14 +1,15 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; -using DependentType = System.Type; -using PrincipalType = System.Type; +using JsonApiDotNetCore.Serialization; +using RightType = System.Type; +using LeftType = System.Type; namespace JsonApiDotNetCore.Hooks { @@ -19,29 +20,28 @@ namespace JsonApiDotNetCore.Hooks /// It creates nodes for each layer. /// Typically, the first layer is homogeneous (all entities have the same type), /// and further nodes can be mixed. - /// /// - internal class TraversalHelper + internal class TraversalHelper : ITraversalHelper { - private readonly IResourceGraph _graph; - private readonly IJsonApiContext _context; + private readonly IdentifiableComparer _comparer = new IdentifiableComparer(); + private readonly IResourceGraph _resourceGraph; + private readonly ITargetedFields _targetedFields; /// /// Keeps track of which entities has already been traversed through, to prevent /// infinite loops in eg cyclic data structures. /// - private Dictionary> _processedEntities; + private Dictionary> _processedEntities; /// /// A mapper from to . /// See the latter for more details. /// - private readonly Dictionary RelationshipProxies = new Dictionary(); - + private readonly Dictionary _relationshipProxies = new Dictionary(); public TraversalHelper( - IResourceGraph graph, - IJsonApiContext context) + IResourceGraph resourceGraph, + ITargetedFields targetedFields) { - _context = context; - _graph = graph; + _targetedFields = targetedFields; + _resourceGraph = resourceGraph; } /// @@ -51,15 +51,15 @@ public TraversalHelper( /// /// The root node. /// Root entities. - /// The 1st type parameter. - public RootNode CreateRootNode(IEnumerable rootEntities) where TEntity : class, IIdentifiable + /// The 1st type parameter. + public RootNode CreateRootNode(IEnumerable rootEntities) where TResource : class, IIdentifiable { - _processedEntities = new Dictionary>(); - RegisterRelationshipProxies(typeof(TEntity)); + _processedEntities = new Dictionary>(); + RegisterRelationshipProxies(typeof(TResource)); var uniqueEntities = ProcessEntities(rootEntities); - var populatedRelationshipsToNextLayer = GetPopulatedRelationships(typeof(TEntity), uniqueEntities.Cast()); - var allRelationshipsFromType = RelationshipProxies.Select(entry => entry.Value).Where(proxy => proxy.PrincipalType == typeof(TEntity)).ToArray(); - return new RootNode(uniqueEntities, populatedRelationshipsToNextLayer, allRelationshipsFromType); + var populatedRelationshipsToNextLayer = GetPopulatedRelationships(typeof(TResource), uniqueEntities.Cast()); + var allRelationshipsFromType = _relationshipProxies.Select(entry => entry.Value).Where(proxy => proxy.LeftType == typeof(TResource)).ToArray(); + return new RootNode(uniqueEntities, populatedRelationshipsToNextLayer, allRelationshipsFromType); } /// @@ -67,9 +67,9 @@ public RootNode CreateRootNode(IEnumerable rootEntiti /// /// The next layer. /// Root node. - public EntityChildLayer CreateNextLayer(IEntityNode rootNode) + public NodeLayer CreateNextLayer(INode rootNode) { - return CreateNextLayer(new IEntityNode[] { rootNode }); + return CreateNextLayer(new INode[] { rootNode }); } /// @@ -77,19 +77,19 @@ public EntityChildLayer CreateNextLayer(IEntityNode rootNode) /// /// The next layer. /// Nodes. - public EntityChildLayer CreateNextLayer(IEnumerable nodes) + public NodeLayer CreateNextLayer(IEnumerable nodes) { /// first extract entities by parsing populated relationships in the entities /// of previous layer - (var principals, var dependents) = ExtractEntities(nodes); + (var lefts, var rights) = ExtractEntities(nodes); /// group them conveniently so we can make ChildNodes of them: - /// there might be several relationship attributes in dependents dictionary - /// that point to the same dependent type. - var principalsGrouped = GroupByDependentTypeOfRelationship(principals); + /// there might be several relationship attributes in rights dictionary + /// that point to the same right type. + var leftsGrouped = GroupByRightTypeOfRelationship(lefts); /// convert the groups into child nodes - var nextNodes = principalsGrouped.Select(entry => + var nextNodes = leftsGrouped.Select(entry => { var nextNodeType = entry.Key; RegisterRelationshipProxies(nextNodeType); @@ -98,85 +98,85 @@ public EntityChildLayer CreateNextLayer(IEnumerable nodes) var relationshipsToPreviousLayer = entry.Value.Select(grouped => { var proxy = grouped.Key; - populatedRelationships.AddRange(GetPopulatedRelationships(nextNodeType, dependents[proxy])); - return CreateRelationshipGroupInstance(nextNodeType, proxy, grouped.Value, dependents[proxy]); + populatedRelationships.AddRange(GetPopulatedRelationships(nextNodeType, rights[proxy])); + return CreateRelationshipGroupInstance(nextNodeType, proxy, grouped.Value, rights[proxy]); }).ToList(); return CreateNodeInstance(nextNodeType, populatedRelationships.ToArray(), relationshipsToPreviousLayer); }).ToList(); /// wrap the child nodes in a EntityChildLayer - return new EntityChildLayer(nextNodes); + return new NodeLayer(nextNodes); } /// /// iterates throug the dictinary and groups the values - /// by matching dependent type of the keys (which are relationshipattributes) + /// by matching right type of the keys (which are relationshipattributes) /// - Dictionary>>> GroupByDependentTypeOfRelationship(Dictionary> relationships) + Dictionary>>> GroupByRightTypeOfRelationship(Dictionary> relationships) { - return relationships.GroupBy(kvp => kvp.Key.DependentType).ToDictionary(gdc => gdc.Key, gdc => gdc.ToList()); + return relationships.GroupBy(kvp => kvp.Key.RightType).ToDictionary(gdc => gdc.Key, gdc => gdc.ToList()); } /// /// Extracts the entities for the current layer by going through all populated relationships - /// of the (principal entities of the previous layer. + /// of the (left entities of the previous layer. /// - (Dictionary>, Dictionary>) ExtractEntities(IEnumerable principalNodes) + (Dictionary>, Dictionary>) ExtractEntities(IEnumerable leftNodes) { - var principalsEntitiesGrouped = new Dictionary>(); // RelationshipAttr_prevlayer->currentlayer => prevLayerEntities - var dependentsEntitiesGrouped = new Dictionary>(); // RelationshipAttr_prevlayer->currentlayer => currentLayerEntities + var leftEntitiesGrouped = new Dictionary>(); // RelationshipAttr_prevlayer->currentlayer => prevLayerEntities + var rightEntitiesGrouped = new Dictionary>(); // RelationshipAttr_prevlayer->currentlayer => currentLayerEntities - foreach (var node in principalNodes) + foreach (var node in leftNodes) { - var principalEntities = node.UniqueEntities; + var leftEntities = node.UniqueEntities; var relationships = node.RelationshipsToNextLayer; - foreach (IIdentifiable principalEntity in principalEntities) + foreach (IIdentifiable leftEntity in leftEntities) { foreach (var proxy in relationships) { - var relationshipValue = proxy.GetValue(principalEntity); + var relationshipValue = proxy.GetValue(leftEntity); // skip this relationship if it's not populated if (!proxy.IsContextRelation && relationshipValue == null) continue; - if (!(relationshipValue is IEnumerable dependentEntities)) + if (!(relationshipValue is IEnumerable rightEntities)) { // in the case of a to-one relationship, the assigned value // will not be a list. We therefore first wrap it in a list. - var list = TypeHelper.CreateListFor(proxy.DependentType); + var list = TypeHelper.CreateListFor(proxy.RightType); if (relationshipValue != null) list.Add(relationshipValue); - dependentEntities = list; + rightEntities = list; } - var uniqueDependentEntities = UniqueInTree(dependentEntities.Cast(), proxy.DependentType); - if (proxy.IsContextRelation || uniqueDependentEntities.Any()) + var uniqueRightEntities = UniqueInTree(rightEntities.Cast(), proxy.RightType); + if (proxy.IsContextRelation || uniqueRightEntities.Any()) { - AddToRelationshipGroup(dependentsEntitiesGrouped, proxy, uniqueDependentEntities); - AddToRelationshipGroup(principalsEntitiesGrouped, proxy, new IIdentifiable[] { principalEntity }); + AddToRelationshipGroup(rightEntitiesGrouped, proxy, uniqueRightEntities); + AddToRelationshipGroup(leftEntitiesGrouped, proxy, new IIdentifiable[] { leftEntity }); } } } } var processEntitiesMethod = GetType().GetMethod(nameof(ProcessEntities), BindingFlags.NonPublic | BindingFlags.Instance); - foreach (var kvp in dependentsEntitiesGrouped) + foreach (var kvp in rightEntitiesGrouped) { - var type = kvp.Key.DependentType; + var type = kvp.Key.RightType; var list = kvp.Value.Cast(type); processEntitiesMethod.MakeGenericMethod(type).Invoke(this, new object[] { list }); } - return (principalsEntitiesGrouped, dependentsEntitiesGrouped); + return (leftEntitiesGrouped, rightEntitiesGrouped); } /// /// Get all populated relationships known in the current tree traversal from a - /// principal type to any dependent type + /// left type to any right type /// /// The relationships. - RelationshipProxy[] GetPopulatedRelationships(PrincipalType principalType, IEnumerable principals) + RelationshipProxy[] GetPopulatedRelationships(LeftType leftType, IEnumerable lefts) { - var relationshipsFromPrincipalToDependent = RelationshipProxies.Select(entry => entry.Value).Where(proxy => proxy.PrincipalType == principalType); - return relationshipsFromPrincipalToDependent.Where(proxy => proxy.IsContextRelation || principals.Any(p => proxy.GetValue(p) != null)).ToArray(); + var relationshipsFromLeftToRight = _relationshipProxies.Select(entry => entry.Value).Where(proxy => proxy.LeftType == leftType); + return relationshipsFromLeftToRight.Where(proxy => proxy.IsContextRelation || lefts.Any(p => proxy.GetValue(p) != null)).ToArray(); } /// @@ -184,10 +184,10 @@ RelationshipProxy[] GetPopulatedRelationships(PrincipalType principalType, IEnum /// /// The entities. /// Incoming entities. - /// The 1st type parameter. - HashSet ProcessEntities(IEnumerable incomingEntities) where TEntity : class, IIdentifiable + /// The 1st type parameter. + HashSet ProcessEntities(IEnumerable incomingEntities) where TResource : class, IIdentifiable { - Type type = typeof(TEntity); + Type type = typeof(TResource); var newEntities = UniqueInTree(incomingEntities, type); RegisterProcessedEntities(newEntities, type); return newEntities; @@ -195,27 +195,26 @@ HashSet ProcessEntities(IEnumerable incomingEntities) /// /// Parses all relationships from to - /// other models in the resource graphs by constructing RelationshipProxies . + /// other models in the resource resourceGraphs by constructing RelationshipProxies . /// /// The type to parse - void RegisterRelationshipProxies(DependentType type) + void RegisterRelationshipProxies(RightType type) { - var contextEntity = _graph.GetContextEntity(type); - foreach (RelationshipAttribute attr in contextEntity.Relationships) + foreach (RelationshipAttribute attr in _resourceGraph.GetRelationships(type)) { if (!attr.CanInclude) continue; - if (!RelationshipProxies.TryGetValue(attr, out RelationshipProxy proxies)) + if (!_relationshipProxies.TryGetValue(attr, out RelationshipProxy proxies)) { - DependentType dependentType = GetDependentTypeFromRelationship(attr); + RightType rightType = GetRightTypeFromRelationship(attr); bool isContextRelation = false; - if (_context.RelationshipsToUpdate != null) isContextRelation = _context.RelationshipsToUpdate.ContainsKey(attr); - var proxy = new RelationshipProxy(attr, dependentType, isContextRelation); - RelationshipProxies[attr] = proxy; + var relationshipsToUpdate = _targetedFields.Relationships; + if (relationshipsToUpdate != null) isContextRelation = relationshipsToUpdate.Contains(attr); + var proxy = new RelationshipProxy(attr, rightType, isContextRelation); + _relationshipProxies[attr] = proxy; } } } - /// /// Registers the processed entities in the dictionary grouped by type /// @@ -249,10 +248,10 @@ HashSet GetProcessedEntities(Type entityType) /// The in tree. /// Entities. /// Entity type. - HashSet UniqueInTree(IEnumerable entities, Type entityType) where TEntity : class, IIdentifiable + HashSet UniqueInTree(IEnumerable entities, Type entityType) where TResource : class, IIdentifiable { - var newEntities = entities.Except(GetProcessedEntities(entityType), ResourceHookExecutor.Comparer).Cast(); - return new HashSet(newEntities); + var newEntities = entities.Except(GetProcessedEntities(entityType), _comparer).Cast(); + return new HashSet(newEntities); } /// @@ -263,13 +262,13 @@ HashSet UniqueInTree(IEnumerable entities, Type entit /// /// The target type for traversal /// Relationship attribute - DependentType GetDependentTypeFromRelationship(RelationshipAttribute attr) + RightType GetRightTypeFromRelationship(RelationshipAttribute attr) { if (attr is HasManyThroughAttribute throughAttr && throughAttr.ThroughType.Inherits(typeof(IIdentifiable))) { return throughAttr.ThroughType; } - return attr.DependentType; + return attr.RightType; } void AddToRelationshipGroup(Dictionary> target, RelationshipProxy proxy, IEnumerable newEntities) @@ -283,32 +282,32 @@ void AddToRelationshipGroup(Dictionary> t } /// - /// Reflective helper method to create an instance of ; + /// Reflective helper method to create an instance of ; /// - IEntityNode CreateNodeInstance(DependentType nodeType, RelationshipProxy[] relationshipsToNext, IEnumerable relationshipsFromPrev) + INode CreateNodeInstance(RightType nodeType, RelationshipProxy[] relationshipsToNext, IEnumerable relationshipsFromPrev) { IRelationshipsFromPreviousLayer prev = CreateRelationshipsFromInstance(nodeType, relationshipsFromPrev); - return (IEntityNode)TypeHelper.CreateInstanceOfOpenType(typeof(ChildNode<>), nodeType, new object[] { relationshipsToNext, prev }); + return (INode)TypeHelper.CreateInstanceOfOpenType(typeof(ChildNode<>), nodeType, new object[] { relationshipsToNext, prev }); } /// - /// Reflective helper method to create an instance of ; + /// Reflective helper method to create an instance of ; /// - IRelationshipsFromPreviousLayer CreateRelationshipsFromInstance(DependentType nodeType, IEnumerable relationshipsFromPrev) + IRelationshipsFromPreviousLayer CreateRelationshipsFromInstance(RightType nodeType, IEnumerable relationshipsFromPrev) { var casted = relationshipsFromPrev.Cast(relationshipsFromPrev.First().GetType()); return (IRelationshipsFromPreviousLayer)TypeHelper.CreateInstanceOfOpenType(typeof(RelationshipsFromPreviousLayer<>), nodeType, new object[] { casted }); } /// - /// Reflective helper method to create an instance of ; + /// Reflective helper method to create an instance of ; /// - IRelationshipGroup CreateRelationshipGroupInstance(Type thisLayerType, RelationshipProxy proxy, List principalEntities, List dependentEntities) + IRelationshipGroup CreateRelationshipGroupInstance(Type thisLayerType, RelationshipProxy proxy, List leftEntities, List rightEntities) { - var dependentEntitiesHashed = TypeHelper.CreateInstanceOfOpenType(typeof(HashSet<>), thisLayerType, dependentEntities.Cast(thisLayerType)); + var rightEntitiesHashed = TypeHelper.CreateInstanceOfOpenType(typeof(HashSet<>), thisLayerType, rightEntities.Cast(thisLayerType)); return (IRelationshipGroup)TypeHelper.CreateInstanceOfOpenType(typeof(RelationshipGroup<>), thisLayerType, - new object[] { proxy, new HashSet(principalEntities), dependentEntitiesHashed }); + new object[] { proxy, new HashSet(leftEntities), rightEntitiesHashed }); } } @@ -316,21 +315,21 @@ IRelationshipGroup CreateRelationshipGroupInstance(Type thisLayerType, Relations /// A helper class that represents all entities in the current layer that /// are being traversed for which hooks will be executed (see IResourceHookExecutor) /// - internal class EntityChildLayer : IEnumerable + internal class NodeLayer : IEnumerable { - readonly List _collection; + readonly List _collection; public bool AnyEntities() { return _collection.Any(n => n.UniqueEntities.Cast().Any()); } - public EntityChildLayer(List nodes) + public NodeLayer(List nodes) { _collection = nodes; } - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { return _collection.GetEnumerator(); } diff --git a/src/JsonApiDotNetCore/Internal/ContextEntity.cs b/src/JsonApiDotNetCore/Internal/ContextEntity.cs deleted file mode 100644 index 867a04350c..0000000000 --- a/src/JsonApiDotNetCore/Internal/ContextEntity.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCore.Internal -{ - public class ContextEntity - { - /// - /// The exposed resource name - /// - public string EntityName { - get; - set; } - - /// - /// The data model type - /// - public Type EntityType { get; set; } - - /// - /// The identity member type - /// - public Type IdentityType { get; set; } - - /// - /// The concrete type. - /// We store this so that we don't need to re-compute the generic type. - /// - public Type ResourceType { get; set; } - - /// - /// Exposed resource attributes - /// - public List Attributes { get; set; } - - /// - /// Exposed resource relationships - /// - public List Relationships { get; set; } - - /// - /// Links to include in resource responses - /// - public Link Links { get; set; } = Link.All; - } -} diff --git a/src/JsonApiDotNetCore/Internal/Contracts/IResourceContextProvider.cs b/src/JsonApiDotNetCore/Internal/Contracts/IResourceContextProvider.cs new file mode 100644 index 0000000000..c975211b6d --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Contracts/IResourceContextProvider.cs @@ -0,0 +1,31 @@ +using System; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Internal.Contracts +{ + /// + /// Responsible for getting s from the . + /// + public interface IResourceContextProvider + { + /// + /// Gets all registered context entities + /// + ResourceContext[] GetResourceContexts(); + + /// + /// Get the resource metadata by the DbSet property name + /// + ResourceContext GetResourceContext(string exposedResourceName); + + /// + /// Get the resource metadata by the resource type + /// + ResourceContext GetResourceContext(Type resourceType); + + /// + /// Get the resource metadata by the resource type + /// + ResourceContext GetResourceContext() where TResource : class, IIdentifiable; + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Internal/Contracts/IResourceGraphExplorer.cs b/src/JsonApiDotNetCore/Internal/Contracts/IResourceGraphExplorer.cs new file mode 100644 index 0000000000..4768e3744c --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Contracts/IResourceGraphExplorer.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Internal.Contracts +{ + /// + /// Responsible for retrieving the exposed resource fields (attributes and + /// relationships) of registered resources in the resource resourceGraph. + /// + public interface IResourceGraph : IResourceContextProvider + { + /// + /// Gets all fields (attributes and relationships) for + /// that are targeted by the selector. If no selector is provided, all + /// exposed fields are returned. + /// + /// The resource for which to retrieve fields + /// Should be of the form: (TResource e) => new { e.Field1, e.Field2 } + List GetFields(Expression> selector = null) where TResource : IIdentifiable; + /// + /// Gets all attributes for + /// that are targeted by the selector. If no selector is provided, all + /// exposed fields are returned. + /// + /// The resource for which to retrieve attributes + /// Should be of the form: (TResource e) => new { e.Attribute1, e.Arttribute2 } + List GetAttributes(Expression> selector = null) where TResource : IIdentifiable; + /// + /// Gets all relationships for + /// that are targeted by the selector. If no selector is provided, all + /// exposed fields are returned. + /// + /// The resource for which to retrieve relationships + /// Should be of the form: (TResource e) => new { e.Relationship1, e.Relationship2 } + List GetRelationships(Expression> selector = null) where TResource : IIdentifiable; + /// + /// Gets all exposed fields (attributes and relationships) for type + /// + /// The resource type. Must extend IIdentifiable. + List GetFields(Type type); + /// + /// Gets all exposed attributes for type + /// + /// The resource type. Must extend IIdentifiable. + List GetAttributes(Type type); + /// + /// Gets all exposed relationships for type + /// + /// The resource type. Must extend IIdentifiable. + List GetRelationships(Type type); + /// + /// Traverses the resource resourceGraph for the inverse relationship of the provided + /// ; + /// + /// + RelationshipAttribute GetInverse(RelationshipAttribute relationship); + } +} diff --git a/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs deleted file mode 100644 index edb7e2444a..0000000000 --- a/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs +++ /dev/null @@ -1,40 +0,0 @@ -// REF: https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.CustomRoutingConvention/NameSpaceRoutingConvention.cs -// REF: https://github.com/aspnet/Mvc/issues/5691 -using System.Reflection; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Extensions; -using Microsoft.AspNetCore.Mvc.ApplicationModels; - -namespace JsonApiDotNetCore.Internal -{ - public class DasherizedRoutingConvention : IApplicationModelConvention - { - private readonly string _namespace; - public DasherizedRoutingConvention(string nspace) - { - _namespace = nspace; - } - - public void Apply(ApplicationModel application) - { - foreach (var controller in application.Controllers) - { - if (IsDasherizedJsonApiController(controller) == false) - continue; - - var template = $"{_namespace}/{controller.ControllerName.Dasherize()}"; - controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel - { - Template = template - }; - } - } - - private bool IsDasherizedJsonApiController(ControllerModel controller) - { - var type = controller.ControllerType; - var notDisabled = type.GetCustomAttribute() == null; - return notDisabled && type.IsSubclassOf(typeof(JsonApiControllerMixin)); - } - } -} diff --git a/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs new file mode 100644 index 0000000000..eb68142e31 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Graph; +using JsonApiDotNetCore.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationModels; + +namespace JsonApiDotNetCore.Internal +{ + /// + /// The default routing convention registers the name of the resource as the route + /// using the that is registered. The default for this is + /// a kebab-case formatter. If the controller directly inherits from JsonApiMixin and there is no + /// resource directly associated, it used the name of the controller instead of the name of the type. + /// + /// + /// public class SomeResourceController: JsonApiController{SomeResource} { } + /// // => /some-resources/relationship/related-resource + /// + /// public class RandomNameController{SomeResource} : JsonApiController{SomeResource} { } + /// // => /some-resources/relationship/related-resource + /// + /// // when using the camelCase formatter: + /// public class SomeResourceController{SomeResource} : JsonApiController{SomeResource} { } + /// // => /someResources/relationship/relatedResource + /// + /// // when inheriting from JsonApiMixin formatter: + /// public class SomeVeryCustomController{SomeResource} : JsonApiMixin { } + /// // => /some-very-customs/relationship/related-resource + /// + public class DefaultRoutingConvention : IJsonApiRoutingConvention, IControllerResourceMapping + { + private readonly string _namespace; + private readonly IResourceNameFormatter _formatter; + private readonly HashSet _registeredTemplates = new HashSet(); + private readonly Dictionary _registeredResources = new Dictionary(); + public DefaultRoutingConvention(IJsonApiOptions options, IResourceNameFormatter formatter) + { + _namespace = options.Namespace; + _formatter = formatter; + } + + /// + public Type GetAssociatedResource(string controllerName) + { + _registeredResources.TryGetValue(controllerName, out Type type); + return type; + } + + /// + public void Apply(ApplicationModel application) + { + foreach (var controller in application.Controllers) + { + var resourceType = GetResourceTypeFromController(controller.ControllerType); + if (resourceType != null) + _registeredResources.Add(controller.ControllerName, resourceType); + + if (RoutingConventionDisabled(controller) == false) + continue; + + var template = TemplateFromResource(controller) ?? TemplateFromController(controller); + if (template == null) + throw new JsonApiSetupException($"Controllers with overlapping route templates detected: {controller.ControllerType.FullName}"); + + controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel { Template = template }; + } + } + + /// + /// Verifies if routing convention should be enabled for this controller + /// + private bool RoutingConventionDisabled(ControllerModel controller) + { + var type = controller.ControllerType; + var notDisabled = type.GetCustomAttribute() == null; + return notDisabled && type.IsSubclassOf(typeof(JsonApiControllerMixin)); + } + + /// + /// Derives a template from the resource type, and checks if this template was already registered. + /// + private string TemplateFromResource(ControllerModel model) + { + if (_registeredResources.TryGetValue(model.ControllerName, out Type resourceType)) + { + var template = $"{_namespace}/{_formatter.FormatResourceName(resourceType)}"; + if (_registeredTemplates.Add(template)) + { + return template; + } + } + return null; + } + + /// + /// Derives a template from the controller name, and checks if this template was already registered. + /// + private string TemplateFromController(ControllerModel model) + { + var template = $"{_namespace}/{_formatter.ApplyCasingConvention(model.ControllerName)}"; + if (_registeredTemplates.Add(template)) + { + return template; + } + else + { + return null; + } + } + + /// + /// Determines the resource associated to a controller by inspecting generic arguments. + /// + private Type GetResourceTypeFromController(Type type) + { + var controllerBase = typeof(ControllerBase); + var jsonApiMixin = typeof(JsonApiControllerMixin); + var target = typeof(BaseJsonApiController<,>); + var identifiable = typeof(IIdentifiable); + var currentBaseType = type; + while (!currentBaseType.IsGenericType || currentBaseType.GetGenericTypeDefinition() != target) + { + var nextBaseType = currentBaseType.BaseType; + + if ( (nextBaseType == controllerBase || nextBaseType == jsonApiMixin) && currentBaseType.IsGenericType) + { + var potentialResource = currentBaseType.GetGenericArguments().FirstOrDefault(t => t.Inherits(identifiable)); + if (potentialResource != null) + { + return potentialResource; + } + } + + currentBaseType = nextBaseType; + if (nextBaseType == null) + { + break; + } + } + return currentBaseType?.GetGenericArguments().First(); + } + } +} diff --git a/src/JsonApiDotNetCore/Internal/Error.cs b/src/JsonApiDotNetCore/Internal/Exceptions/Error.cs similarity index 100% rename from src/JsonApiDotNetCore/Internal/Error.cs rename to src/JsonApiDotNetCore/Internal/Exceptions/Error.cs diff --git a/src/JsonApiDotNetCore/Internal/ErrorCollection.cs b/src/JsonApiDotNetCore/Internal/Exceptions/ErrorCollection.cs similarity index 100% rename from src/JsonApiDotNetCore/Internal/ErrorCollection.cs rename to src/JsonApiDotNetCore/Internal/Exceptions/ErrorCollection.cs diff --git a/src/JsonApiDotNetCore/Internal/Exceptions.cs b/src/JsonApiDotNetCore/Internal/Exceptions/Exceptions.cs similarity index 100% rename from src/JsonApiDotNetCore/Internal/Exceptions.cs rename to src/JsonApiDotNetCore/Internal/Exceptions/Exceptions.cs diff --git a/src/JsonApiDotNetCore/Internal/JsonApiException.cs b/src/JsonApiDotNetCore/Internal/Exceptions/JsonApiException.cs similarity index 98% rename from src/JsonApiDotNetCore/Internal/JsonApiException.cs rename to src/JsonApiDotNetCore/Internal/Exceptions/JsonApiException.cs index 0f1f06dfdb..9f94800a98 100644 --- a/src/JsonApiDotNetCore/Internal/JsonApiException.cs +++ b/src/JsonApiDotNetCore/Internal/Exceptions/JsonApiException.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; namespace JsonApiDotNetCore.Internal { diff --git a/src/JsonApiDotNetCore/Internal/JsonApiExceptionFactory.cs b/src/JsonApiDotNetCore/Internal/Exceptions/JsonApiExceptionFactory.cs similarity index 100% rename from src/JsonApiDotNetCore/Internal/JsonApiExceptionFactory.cs rename to src/JsonApiDotNetCore/Internal/Exceptions/JsonApiExceptionFactory.cs diff --git a/src/JsonApiDotNetCore/Internal/JsonApiSetupException.cs b/src/JsonApiDotNetCore/Internal/Exceptions/JsonApiSetupException.cs similarity index 100% rename from src/JsonApiDotNetCore/Internal/JsonApiSetupException.cs rename to src/JsonApiDotNetCore/Internal/Exceptions/JsonApiSetupException.cs diff --git a/src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs b/src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs deleted file mode 100644 index 5145621058..0000000000 --- a/src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading.Tasks; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage; - -namespace JsonApiDotNetCore.Internal.Generics -{ - // TODO: consider renaming to PatchRelationshipService (or something) - public interface IGenericProcessor - { - Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds); - } - - /// - /// A special processor that gets instantiated for a generic type (<T>) - /// when the actual type is not known until runtime. Specifically, this is used for updating - /// relationships. - /// - public class GenericProcessor : IGenericProcessor where T : class - { - private readonly DbContext _context; - public GenericProcessor(IDbContextResolver contextResolver) - { - _context = contextResolver.GetContext(); - } - - public virtual async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds) - { - if (relationship is HasManyThroughAttribute hasManyThrough && parent is IIdentifiable identifiableParent) - { - await SetHasManyThroughRelationshipAsync(identifiableParent, hasManyThrough, relationshipIds); - } - else - { - await SetRelationshipsAsync(parent, relationship, relationshipIds); - } - } - - private async Task SetHasManyThroughRelationshipAsync(IIdentifiable identifiableParent, HasManyThroughAttribute hasManyThrough, IEnumerable relationshipIds) - { - // we need to create a transaction for the HasManyThrough case so we can get and remove any existing - // join entities and only commit if all operations are successful - using(var transaction = await _context.GetCurrentOrCreateTransactionAsync()) - { - // ArticleTag - ParameterExpression parameter = Expression.Parameter(hasManyThrough.ThroughType); - - // ArticleTag.ArticleId - Expression property = Expression.Property(parameter, hasManyThrough.LeftIdProperty); - - // article.Id - var parentId = TypeHelper.ConvertType(identifiableParent.StringId, hasManyThrough.LeftIdProperty.PropertyType); - Expression target = Expression.Constant(parentId); - - // ArticleTag.ArticleId.Equals(article.Id) - Expression equals = Expression.Call(property, "Equals", null, target); - - var lambda = Expression.Lambda>(equals, parameter); - - // TODO: we shouldn't need to do this instead we should try updating the existing? - // the challenge here is if a composite key is used, then we will fail to - // create due to a unique key violation - var oldLinks = _context - .Set() - .Where(lambda.Compile()) - .ToList(); - - _context.RemoveRange(oldLinks); - - var newLinks = relationshipIds.Select(x => { - var link = Activator.CreateInstance(hasManyThrough.ThroughType); - hasManyThrough.LeftIdProperty.SetValue(link, TypeHelper.ConvertType(parentId, hasManyThrough.LeftIdProperty.PropertyType)); - hasManyThrough.RightIdProperty.SetValue(link, TypeHelper.ConvertType(x, hasManyThrough.RightIdProperty.PropertyType)); - return link; - }); - - _context.AddRange(newLinks); - await _context.SaveChangesAsync(); - - transaction.Commit(); - } - } - - private async Task SetRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds) - { - if (relationship.IsHasMany) - { - var entities = _context.Set().Where(x => relationshipIds.Contains(((IIdentifiable)x).StringId)).ToList(); - relationship.SetValue(parent, entities); - } - else - { - var entity = _context.Set().SingleOrDefault(x => relationshipIds.First() == ((IIdentifiable)x).StringId); - relationship.SetValue(parent, entity); - } - - await _context.SaveChangesAsync(); - } - } -} diff --git a/src/JsonApiDotNetCore/Internal/Generics/GenericProcessorFactory.cs b/src/JsonApiDotNetCore/Internal/Generics/GenericServiceFactory.cs similarity index 50% rename from src/JsonApiDotNetCore/Internal/Generics/GenericProcessorFactory.cs rename to src/JsonApiDotNetCore/Internal/Generics/GenericServiceFactory.cs index f6849b178b..bc834f4733 100644 --- a/src/JsonApiDotNetCore/Internal/Generics/GenericProcessorFactory.cs +++ b/src/JsonApiDotNetCore/Internal/Generics/GenericServiceFactory.cs @@ -8,45 +8,45 @@ namespace JsonApiDotNetCore.Internal.Generics /// are not known until runtime. The typical use case would be for /// accessing relationship data or resolving operations processors. /// - public interface IGenericProcessorFactory + public interface IGenericServiceFactory { /// /// Constructs the generic type and locates the service, then casts to TInterface /// /// /// - /// GetProcessor<IGenericProcessor>(typeof(GenericProcessor<>), typeof(TResource)); + /// Get<IGenericProcessor>(typeof(GenericProcessor<>), typeof(TResource)); /// /// - TInterface GetProcessor(Type openGenericType, Type resourceType); + TInterface Get(Type openGenericType, Type resourceType); /// /// Constructs the generic type and locates the service, then casts to TInterface /// /// /// - /// GetProcessor<IGenericProcessor>(typeof(GenericProcessor<,>), typeof(TResource), typeof(TId)); + /// Get<IGenericProcessor>(typeof(GenericProcessor<,>), typeof(TResource), typeof(TId)); /// /// - TInterface GetProcessor(Type openGenericType, Type resourceType, Type keyType); + TInterface Get(Type openGenericType, Type resourceType, Type keyType); } - public class GenericProcessorFactory : IGenericProcessorFactory + public class GenericServiceFactory : IGenericServiceFactory { private readonly IServiceProvider _serviceProvider; - public GenericProcessorFactory(IScopedServiceProvider serviceProvider) + public GenericServiceFactory(IScopedServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } - public TInterface GetProcessor(Type openGenericType, Type resourceType) - => GetProcessorInternal(openGenericType, resourceType); + public TInterface Get(Type openGenericType, Type resourceType) + => GetInternal(openGenericType, resourceType); - public TInterface GetProcessor(Type openGenericType, Type resourceType, Type keyType) - => GetProcessorInternal(openGenericType, resourceType, keyType); + public TInterface Get(Type openGenericType, Type resourceType, Type keyType) + => GetInternal(openGenericType, resourceType, keyType); - private TInterface GetProcessorInternal(Type openGenericType, params Type[] types) + private TInterface GetInternal(Type openGenericType, params Type[] types) { var concreteType = openGenericType.MakeGenericType(types); diff --git a/src/JsonApiDotNetCore/Internal/Generics/RepositoryRelationshipUpdateHelper.cs b/src/JsonApiDotNetCore/Internal/Generics/RepositoryRelationshipUpdateHelper.cs new file mode 100644 index 0000000000..ec4a4f0bbb --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Generics/RepositoryRelationshipUpdateHelper.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Models; +using Microsoft.EntityFrameworkCore; + +namespace JsonApiDotNetCore.Internal.Generics +{ + /// + /// A special helper that processes updates of relationships + /// + /// + /// This service required to be able translate involved expressions into queries + /// instead of having them evaluated on the client side. In particular, for all three types of relationship + /// a lookup is performed based on an id. Expressions that use IIdentifiable.StringId can never + /// be translated into queries because this property only exists at runtime after the query is performed. + /// We will have to build expression trees if we want to use IIdentifiable{TId}.TId, for which we minimally a + /// generic execution to DbContext.Set{T}(). + /// + public interface IRepositoryRelationshipUpdateHelper + { + /// + /// Processes updates of relationships + /// + Task UpdateRelationshipAsync(IIdentifiable parent, RelationshipAttribute relationship, IEnumerable relationshipIds); + } + + /// + public class RepositoryRelationshipUpdateHelper : IRepositoryRelationshipUpdateHelper where TRelatedResource : class + { + private readonly DbContext _context; + public RepositoryRelationshipUpdateHelper(IDbContextResolver contextResolver) + { + _context = contextResolver.GetContext(); + } + + /// + public virtual async Task UpdateRelationshipAsync(IIdentifiable parent, RelationshipAttribute relationship, IEnumerable relationshipIds) + { + if (relationship is HasManyThroughAttribute hasManyThrough) + await UpdateManyToManyAsync(parent, hasManyThrough, relationshipIds); + else if (relationship is HasManyAttribute) + await UpdateOneToManyAsync(parent, relationship, relationshipIds); + else + await UpdateOneToOneAsync(parent, relationship, relationshipIds); + } + + private async Task UpdateOneToOneAsync(IIdentifiable parent, RelationshipAttribute relationship, IEnumerable relationshipIds) + { + TRelatedResource value = null; + if (relationshipIds.Any()) + { // newOwner.id + var target = Expression.Constant(TypeHelper.ConvertType(relationshipIds.First(), TypeHelper.GetIdentifierType(relationship.RightType))); + // (Person p) => ... + ParameterExpression parameter = Expression.Parameter(typeof(TRelatedResource)); + // (Person p) => p.Id + Expression idMember = Expression.Property(parameter, nameof(Identifiable.Id)); + // newOwner.Id.Equals(p.Id) + Expression callEquals = Expression.Call(idMember, nameof(object.Equals), null, target); + var equalsLambda = Expression.Lambda>(callEquals, parameter); + value = await _context.Set().FirstOrDefaultAsync(equalsLambda); + } + relationship.SetValue(parent, value); + } + + private async Task UpdateOneToManyAsync(IIdentifiable parent, RelationshipAttribute relationship, IEnumerable relationshipIds) + { + var value = new List(); + if (relationshipIds.Any()) + { // [1, 2, 3] + var target = Expression.Constant(TypeHelper.ConvertListType(relationshipIds, TypeHelper.GetIdentifierType(relationship.RightType))); + // (Person p) => ... + ParameterExpression parameter = Expression.Parameter(typeof(TRelatedResource)); + // (Person p) => p.Id + Expression idMember = Expression.Property(parameter, nameof(Identifiable.Id)); + // [1,2,3].Contains(p.Id) + var callContains = Expression.Call(typeof(Enumerable), nameof(Enumerable.Contains), new[] { idMember.Type }, target, idMember); + var containsLamdda = Expression.Lambda>(callContains, parameter); + value = await _context.Set().Where(containsLamdda).ToListAsync(); + } + relationship.SetValue(parent, value); + } + + private async Task UpdateManyToManyAsync(IIdentifiable parent, HasManyThroughAttribute relationship, IEnumerable relationshipIds) + { + // we need to create a transaction for the HasManyThrough case so we can get and remove any existing + // join entities and only commit if all operations are successful + var transaction = await _context.GetCurrentOrCreateTransactionAsync(); + // ArticleTag + ParameterExpression parameter = Expression.Parameter(relationship.ThroughType); + // ArticleTag.ArticleId + Expression idMember = Expression.Property(parameter, relationship.LeftIdProperty); + // article.Id + var parentId = TypeHelper.ConvertType(parent.StringId, relationship.LeftIdProperty.PropertyType); + Expression target = Expression.Constant(parentId); + // ArticleTag.ArticleId.Equals(article.Id) + Expression callEquals = Expression.Call(idMember, "Equals", null, target); + var lambda = Expression.Lambda>(callEquals, parameter); + // TODO: we shouldn't need to do this instead we should try updating the existing? + // the challenge here is if a composite key is used, then we will fail to + // create due to a unique key violation + var oldLinks = _context + .Set() + .Where(lambda.Compile()) + .ToList(); + + _context.RemoveRange(oldLinks); + + var newLinks = relationshipIds.Select(x => + { + var link = Activator.CreateInstance(relationship.ThroughType); + relationship.LeftIdProperty.SetValue(link, TypeHelper.ConvertType(parentId, relationship.LeftIdProperty.PropertyType)); + relationship.RightIdProperty.SetValue(link, TypeHelper.ConvertType(x, relationship.RightIdProperty.PropertyType)); + return link; + }); + + _context.AddRange(newLinks); + await _context.SaveChangesAsync(); + transaction.Commit(); + } + } +} diff --git a/src/JsonApiDotNetCore/Internal/IControllerResourceMapping.cs b/src/JsonApiDotNetCore/Internal/IControllerResourceMapping.cs new file mode 100644 index 0000000000..347a85650c --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/IControllerResourceMapping.cs @@ -0,0 +1,15 @@ +using System; + +namespace JsonApiDotNetCore.Internal +{ + /// + /// Registery of which resource is associated with which controller. + /// + public interface IControllerResourceMapping + { + /// + /// Get the associated resource with the controller with the provided controller name + /// + Type GetAssociatedResource(string controllerName); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Internal/IJsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/IJsonApiRoutingConvention.cs new file mode 100644 index 0000000000..00eed0b4c0 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/IJsonApiRoutingConvention.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc.ApplicationModels; + +namespace JsonApiDotNetCore.Internal +{ + /// + /// Service for specifying which routing convention to use. This can be overriden to customize + /// the relation between controllers and mapped routes. + /// + public interface IJsonApiRoutingConvention : IApplicationModelConvention, IControllerResourceMapping { } +} diff --git a/src/JsonApiDotNetCore/Internal/IdentifiableComparer.cs b/src/JsonApiDotNetCore/Internal/IdentifiableComparer.cs index a830e2aec5..60793829b8 100644 --- a/src/JsonApiDotNetCore/Internal/IdentifiableComparer.cs +++ b/src/JsonApiDotNetCore/Internal/IdentifiableComparer.cs @@ -1,16 +1,14 @@ using JsonApiDotNetCore.Models; -using System; using System.Collections.Generic; -using System.Text; namespace JsonApiDotNetCore.Internal { /// /// Compares `IIdentifiable` with each other based on ID /// - /// The type to compare public class IdentifiableComparer : IEqualityComparer { + internal static readonly IdentifiableComparer Instance = new IdentifiableComparer(); public bool Equals(IIdentifiable x, IIdentifiable y) { return x.StringId == y.StringId; diff --git a/src/JsonApiDotNetCore/Internal/InverseRelationships.cs b/src/JsonApiDotNetCore/Internal/InverseRelationships.cs index bec80243d9..3aa999f9bb 100644 --- a/src/JsonApiDotNetCore/Internal/InverseRelationships.cs +++ b/src/JsonApiDotNetCore/Internal/InverseRelationships.cs @@ -1,5 +1,5 @@ -using System; using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; @@ -23,17 +23,18 @@ public interface IInverseRelationships /// deal with resolving the inverse relationships. /// void Resolve(); + } /// public class InverseRelationships : IInverseRelationships { - private readonly ResourceGraph _graph; + private readonly IResourceContextProvider _provider; private readonly IDbContextResolver _resolver; - public InverseRelationships(IResourceGraph graph, IDbContextResolver resolver = null) + public InverseRelationships(IResourceContextProvider provider, IDbContextResolver resolver = null) { - _graph = (ResourceGraph)graph; + _provider = provider; _resolver = resolver; } @@ -44,9 +45,9 @@ public void Resolve() { DbContext context = _resolver.GetContext(); - foreach (ContextEntity ce in _graph.Entities) + foreach (ResourceContext ce in _provider.GetResourceContexts()) { - IEntityType meta = context.Model.FindEntityType(ce.EntityType); + IEntityType meta = context.Model.FindEntityType(ce.ResourceType); if (meta == null) continue; foreach (var attr in ce.Relationships) { @@ -62,10 +63,6 @@ public void Resolve() /// If EF Core is not being used, we're expecting the resolver to not be registered. /// /// true, if entity framework core was enabled, false otherwise. - /// Resolver. - private bool EntityFrameworkCoreIsEnabled() - { - return _resolver != null; - } + private bool EntityFrameworkCoreIsEnabled() => _resolver != null; } } diff --git a/src/JsonApiDotNetCore/Internal/JsonApiRouteHandler.cs b/src/JsonApiDotNetCore/Internal/JsonApiRouteHandler.cs deleted file mode 100644 index 132f8d2042..0000000000 --- a/src/JsonApiDotNetCore/Internal/JsonApiRouteHandler.cs +++ /dev/null @@ -1,83 +0,0 @@ -// REF: https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcRouteHandler.cs -using System; -using System.Threading.Tasks; -using JsonApiDotNetCore.Extensions; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Routing; - -namespace JsonApiDotNetCore.Internal -{ - public class JsonApiRouteHandler : IRouter - { - private readonly IActionContextAccessor _actionContextAccessor; - private readonly IActionInvokerFactory _actionInvokerFactory; - private readonly IActionSelector _actionSelector; - - public JsonApiRouteHandler( - IActionInvokerFactory actionInvokerFactory, - IActionSelector actionSelector) - : this(actionInvokerFactory, actionSelector, actionContextAccessor: null) - { - } - - public JsonApiRouteHandler( - IActionInvokerFactory actionInvokerFactory, - IActionSelector actionSelector, - IActionContextAccessor actionContextAccessor) - { - // The IActionContextAccessor is optional. We want to avoid the overhead of using CallContext - // if possible. - _actionContextAccessor = actionContextAccessor; - _actionInvokerFactory = actionInvokerFactory; - _actionSelector = actionSelector; - } - - public VirtualPathData GetVirtualPath(VirtualPathContext context) - { - if (context == null) - throw new ArgumentNullException(nameof(context)); - - // We return null here because we're not responsible for generating the url, the route is. - return null; - } - - public Task RouteAsync(RouteContext context) - { - if (context == null) - throw new ArgumentNullException(nameof(context)); - - var candidates = _actionSelector.SelectCandidates(context); - if (candidates == null || candidates.Count == 0) - { - return Task.CompletedTask; - } - - var actionDescriptor = _actionSelector.SelectBestCandidate(context, candidates); - if (actionDescriptor == null) - { - return Task.CompletedTask; - } - - context.Handler = (c) => - { - var routeData = c.GetRouteData(); - - foreach(var routeValue in routeData.Values) - routeData.Values[routeValue.Key] = routeValue.Value.ToString().ToProperCase(); - - var actionContext = new ActionContext(context.HttpContext, routeData, actionDescriptor); - if (_actionContextAccessor != null) - { - _actionContextAccessor.ActionContext = actionContext; - } - - var invoker = _actionInvokerFactory.CreateInvoker(actionContext); - - return invoker.InvokeAsync(); - }; - - return Task.CompletedTask; - } - } -} diff --git a/src/JsonApiDotNetCore/Internal/PageManager.cs b/src/JsonApiDotNetCore/Internal/PageManager.cs deleted file mode 100644 index d27fc158fd..0000000000 --- a/src/JsonApiDotNetCore/Internal/PageManager.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCore.Internal -{ - public class PageManager - { - public int? TotalRecords { get; set; } - public int PageSize { get; set; } - public int DefaultPageSize { get; set; } - public int CurrentPage { get; set; } - public bool IsPaginated => PageSize > 0; - public int TotalPages => (TotalRecords == null) ? -1 : (int)Math.Ceiling(decimal.Divide(TotalRecords.Value, PageSize)); - - public RootLinks GetPageLinks(LinkBuilder linkBuilder) - { - if (ShouldIncludeLinksObject()) - return null; - - var rootLinks = new RootLinks(); - - if (CurrentPage > 1) - rootLinks.First = linkBuilder.GetPageLink(1, PageSize); - - if (CurrentPage > 1) - rootLinks.Prev = linkBuilder.GetPageLink(CurrentPage - 1, PageSize); - - if (CurrentPage < TotalPages) - rootLinks.Next = linkBuilder.GetPageLink(CurrentPage + 1, PageSize); - - if (TotalPages > 0) - rootLinks.Last = linkBuilder.GetPageLink(TotalPages, PageSize); - - return rootLinks; - } - - private bool ShouldIncludeLinksObject() => (!IsPaginated || ((CurrentPage == 1 || CurrentPage == 0) && TotalPages <= 0)); - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs deleted file mode 100644 index 3205e1e01a..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Internal.Query -{ - public class AttrFilterQuery : BaseFilterQuery - { - public AttrFilterQuery( - IJsonApiContext jsonApiContext, - FilterQuery filterQuery) - : base(jsonApiContext, filterQuery) - { - if (Attribute == null) - throw new JsonApiException(400, $"'{filterQuery.Attribute}' is not a valid attribute."); - - if (Attribute.IsFilterable == false) - throw new JsonApiException(400, $"Filter is not allowed for attribute '{Attribute.PublicAttributeName}'."); - } - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs deleted file mode 100644 index 341b7e15c0..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs +++ /dev/null @@ -1,23 +0,0 @@ -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Internal.Query -{ - public class AttrSortQuery : BaseAttrQuery - { - public AttrSortQuery( - IJsonApiContext jsonApiContext, - SortQuery sortQuery) - :base(jsonApiContext, sortQuery) - { - if (Attribute == null) - throw new JsonApiException(400, $"'{sortQuery.Attribute}' is not a valid attribute."); - - if (Attribute.IsSortable == false) - throw new JsonApiException(400, $"Sort is not allowed for attribute '{Attribute.PublicAttributeName}'."); - - Direction = sortQuery.Direction; - } - - public SortDirection Direction { get; } - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs deleted file mode 100644 index e7eeb83e68..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs +++ /dev/null @@ -1,71 +0,0 @@ -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; -using System; -using System.Linq; - -namespace JsonApiDotNetCore.Internal.Query -{ - /// - /// Abstract class to make available shared properties of all query implementations - /// It elimines boilerplate of providing specified type(AttrQuery or RelatedAttrQuery) - /// while filter and sort operations and eliminates plenty of methods to keep DRY principles - /// - public abstract class BaseAttrQuery - { - private readonly IJsonApiContext _jsonApiContext; - - public BaseAttrQuery(IJsonApiContext jsonApiContext, BaseQuery baseQuery) - { - _jsonApiContext = jsonApiContext ?? throw new ArgumentNullException(nameof(jsonApiContext)); - - if(_jsonApiContext.RequestEntity == null) - throw new ArgumentException($"{nameof(IJsonApiContext)}.{nameof(_jsonApiContext.RequestEntity)} cannot be null. " - + "This property contains the ResourceGraph node for the requested entity. " - + "If this is a unit test, you need to mock this member. " - + "See this issue to check the current status of improved test guidelines: " - + "https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/251", nameof(jsonApiContext)); - - if(_jsonApiContext.ResourceGraph == null) - throw new ArgumentException($"{nameof(IJsonApiContext)}.{nameof(_jsonApiContext.ResourceGraph)} cannot be null. " - + "If this is a unit test, you need to construct a graph containing the resources being tested. " - + "See this issue to check the current status of improved test guidelines: " - + "https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/251", nameof(jsonApiContext)); - - if (baseQuery.IsAttributeOfRelationship) - { - Relationship = GetRelationship(baseQuery.Relationship); - Attribute = GetAttribute(Relationship, baseQuery.Attribute); - } - else - { - Attribute = GetAttribute(baseQuery.Attribute); - } - - } - - public AttrAttribute Attribute { get; } - public RelationshipAttribute Relationship { get; } - public bool IsAttributeOfRelationship => Relationship != null; - - public string GetPropertyPath() - { - if (IsAttributeOfRelationship) - return string.Format("{0}.{1}", Relationship.InternalRelationshipName, Attribute.InternalAttributeName); - else - return Attribute.InternalAttributeName; - } - - private AttrAttribute GetAttribute(string attribute) - => _jsonApiContext.RequestEntity.Attributes.FirstOrDefault(attr => attr.Is(attribute)); - - private RelationshipAttribute GetRelationship(string propertyName) - => _jsonApiContext.RequestEntity.Relationships.FirstOrDefault(r => r.Is(propertyName)); - - private AttrAttribute GetAttribute(RelationshipAttribute relationship, string attribute) - { - var relatedContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.DependentType); - return relatedContextEntity.Attributes - .FirstOrDefault(a => a.Is(attribute)); - } - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs deleted file mode 100644 index f7e308369e..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs +++ /dev/null @@ -1,31 +0,0 @@ -using JsonApiDotNetCore.Services; -using System; - -namespace JsonApiDotNetCore.Internal.Query -{ - public class BaseFilterQuery : BaseAttrQuery - { - public BaseFilterQuery( - IJsonApiContext jsonApiContext, - FilterQuery filterQuery) - : base(jsonApiContext, filterQuery) - { - PropertyValue = filterQuery.Value; - FilterOperation = GetFilterOperation(filterQuery.Operation); - } - - public string PropertyValue { get; } - public FilterOperations FilterOperation { get; } - - private FilterOperations GetFilterOperation(string prefix) - { - if (prefix.Length == 0) return FilterOperations.eq; - - if (Enum.TryParse(prefix, out FilterOperations opertion) == false) - throw new JsonApiException(400, $"Invalid filter prefix '{prefix}'"); - - return opertion; - } - - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseQuery.cs index 90830196c4..75c760ed03 100644 --- a/src/JsonApiDotNetCore/Internal/Query/BaseQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/BaseQuery.cs @@ -1,16 +1,16 @@ -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; -using System; -using System.Linq; - namespace JsonApiDotNetCore.Internal.Query { + /// + /// represents what FilterQuery and SortQuery have in common: a target. + /// (sort=TARGET, or filter[TARGET]=123). + /// public abstract class BaseQuery { - public BaseQuery(string attribute) + protected BaseQuery(string target) { - var properties = attribute.Split(QueryConstants.DOT); - if(properties.Length > 1) + Target = target; + var properties = target.Split(QueryConstants.DOT); + if (properties.Length > 1) { Relationship = properties[0]; Attribute = properties[1]; @@ -19,8 +19,8 @@ public BaseQuery(string attribute) Attribute = properties[0]; } + public string Target { get; } public string Attribute { get; } public string Relationship { get; } - public bool IsAttributeOfRelationship => Relationship != null; } } diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseQueryContext.cs b/src/JsonApiDotNetCore/Internal/Query/BaseQueryContext.cs new file mode 100644 index 0000000000..75b8dd25db --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/BaseQueryContext.cs @@ -0,0 +1,31 @@ +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Internal.Query +{ + /// + /// A context class that provides extra meta data for a + /// that is used when applying url queries internally. + /// + public abstract class BaseQueryContext where TQuery : BaseQuery + { + protected BaseQueryContext(TQuery query) + { + Query = query; + } + + public bool IsCustom { get; internal set; } + public AttrAttribute Attribute { get; internal set; } + public RelationshipAttribute Relationship { get; internal set; } + public bool IsAttributeOfRelationship => Relationship != null; + + public TQuery Query { get; } + + public string GetPropertyPath() + { + if (IsAttributeOfRelationship) + return string.Format("{0}.{1}", Relationship.InternalRelationshipName, Attribute.InternalAttributeName); + + return Attribute.InternalAttributeName; + } + } +} diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs b/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs index 60ae0af012..aee022cd20 100644 --- a/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs +++ b/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs @@ -1,7 +1,7 @@ // ReSharper disable InconsistentNaming namespace JsonApiDotNetCore.Internal.Query { - public enum FilterOperations + public enum FilterOperation { eq = 0, lt = 1, diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs index e1b53cd47d..40588e672b 100644 --- a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs @@ -1,31 +1,21 @@ -using System; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Models; - namespace JsonApiDotNetCore.Internal.Query { /// - /// Allows you to filter the query, via the methods shown at - /// HERE + /// Internal representation of the raw articles?filter[X]=Y query from the URL. /// public class FilterQuery : BaseQuery { - /// - /// Allows you to filter the query, via the methods shown at - /// HERE - /// - /// the json attribute you want to filter on - /// the value this attribute should be - /// possible values: eq, ne, lt, gt, le, ge, like, in (default) - public FilterQuery(string attribute, string value, string operation) - : base(attribute) + public FilterQuery(string target, string value, string operation) + : base(target) { Value = value; Operation = operation; } public string Value { get; set; } + /// + /// See . Can also be a custom operation. + /// public string Operation { get; set; } - } } diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterQueryContext.cs b/src/JsonApiDotNetCore/Internal/Query/FilterQueryContext.cs new file mode 100644 index 0000000000..e1754d4ca7 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/FilterQueryContext.cs @@ -0,0 +1,24 @@ +using System; + +namespace JsonApiDotNetCore.Internal.Query +{ + /// + /// Wrapper class for filter queries. Provides the internals + /// with metadata it needs to perform the url filter queries on the targeted dataset. + /// + public class FilterQueryContext : BaseQueryContext + { + public FilterQueryContext(FilterQuery query) : base(query) { } + public object CustomQuery { get; set; } + public string Value => Query.Value; + public FilterOperation Operation + { + get + { + if (!Enum.TryParse(Query.Operation, out var result)) + return FilterOperation.eq; + return result; + } + } + } +} diff --git a/src/JsonApiDotNetCore/Internal/Query/PageQuery.cs b/src/JsonApiDotNetCore/Internal/Query/PageQuery.cs deleted file mode 100644 index 7c09d4c386..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/PageQuery.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace JsonApiDotNetCore.Internal.Query -{ - public class PageQuery - { - public int PageSize { get; set; } - public int PageOffset { get; set; } = 1; - } -} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Internal/Query/QueryConstants.cs b/src/JsonApiDotNetCore/Internal/Query/QueryConstants.cs index 25913ab3e6..14189017da 100644 --- a/src/JsonApiDotNetCore/Internal/Query/QueryConstants.cs +++ b/src/JsonApiDotNetCore/Internal/Query/QueryConstants.cs @@ -1,16 +1,11 @@ -namespace JsonApiDotNetCore.Internal.Query{ +namespace JsonApiDotNetCore.Internal.Query +{ public static class QueryConstants { - public const string FILTER = "filter"; - public const string SORT = "sort"; - public const string INCLUDE = "include"; - public const string PAGE = "page"; - public const string FIELDS = "fields"; public const char OPEN_BRACKET = '['; public const char CLOSE_BRACKET = ']'; public const char COMMA = ','; public const char COLON = ':'; public const string COLON_STR = ":"; public const char DOT = '.'; - } } diff --git a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs b/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs deleted file mode 100644 index 1a60879667..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; - -namespace JsonApiDotNetCore.Internal.Query -{ - public class QuerySet - { - public List Filters { get; set; } = new List(); - public PageQuery PageQuery { get; set; } = new PageQuery(); - public List SortParameters { get; set; } = new List(); - public List IncludedRelationships { get; set; } = new List(); - public List Fields { get; set; } = new List(); - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs deleted file mode 100644 index 8fef8de693..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Internal.Query -{ - public class RelatedAttrFilterQuery : BaseFilterQuery - { - public RelatedAttrFilterQuery( - IJsonApiContext jsonApiContext, - FilterQuery filterQuery) - :base(jsonApiContext, filterQuery) - { - if (Relationship == null) - throw new JsonApiException(400, $"{filterQuery.Relationship} is not a valid relationship on {jsonApiContext.RequestEntity.EntityName}."); - - if (Attribute == null) - throw new JsonApiException(400, $"'{filterQuery.Attribute}' is not a valid attribute."); - - if (Attribute.IsFilterable == false) - throw new JsonApiException(400, $"Filter is not allowed for attribute '{Attribute.PublicAttributeName}'."); - - } - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs deleted file mode 100644 index 4215382c80..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs +++ /dev/null @@ -1,26 +0,0 @@ -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Internal.Query -{ - public class RelatedAttrSortQuery : BaseAttrQuery - { - public RelatedAttrSortQuery( - IJsonApiContext jsonApiContext, - SortQuery sortQuery) - :base(jsonApiContext, sortQuery) - { - if (Relationship == null) - throw new JsonApiException(400, $"{sortQuery.Relationship} is not a valid relationship on {jsonApiContext.RequestEntity.EntityName}."); - - if (Attribute == null) - throw new JsonApiException(400, $"'{sortQuery.Attribute}' is not a valid attribute."); - - if (Attribute.IsSortable == false) - throw new JsonApiException(400, $"Sort is not allowed for attribute '{Attribute.PublicAttributeName}'."); - - Direction = sortQuery.Direction; - } - - public SortDirection Direction { get; } - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs index 7194b6e948..840de80ddb 100644 --- a/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs @@ -1,15 +1,12 @@ -using JsonApiDotNetCore.Models; -using System; - -namespace JsonApiDotNetCore.Internal.Query +namespace JsonApiDotNetCore.Internal.Query { /// - /// An internal representation of the raw sort query. + /// Internal representation of the raw articles?sort[field] query from the URL. /// public class SortQuery : BaseQuery { - public SortQuery(SortDirection direction, string attribute) - : base(attribute) + public SortQuery(string target, SortDirection direction) + : base(target) { Direction = direction; } diff --git a/src/JsonApiDotNetCore/Internal/Query/SortQueryContext.cs b/src/JsonApiDotNetCore/Internal/Query/SortQueryContext.cs new file mode 100644 index 0000000000..ce1395102d --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/SortQueryContext.cs @@ -0,0 +1,12 @@ +namespace JsonApiDotNetCore.Internal.Query +{ + /// + /// Wrapper class for sort queries. Provides the internals + /// with metadata it needs to perform the url sort queries on the targeted dataset. + /// + public class SortQueryContext : BaseQueryContext + { + public SortQueryContext(SortQuery sortQuery) : base(sortQuery) { } + public SortDirection Direction => Query.Direction; + } +} diff --git a/src/JsonApiDotNetCore/Internal/ResourceContext.cs b/src/JsonApiDotNetCore/Internal/ResourceContext.cs new file mode 100644 index 0000000000..91934ca070 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/ResourceContext.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.Links; + +namespace JsonApiDotNetCore.Internal +{ + public class ResourceContext + { + /// + /// The exposed resource name + /// + public string ResourceName { get; set; } + + /// + /// The data model type + /// + public Type ResourceType { get; set; } + + /// + /// The identity member type + /// + public Type IdentityType { get; set; } + + /// + /// The concrete type. + /// We store this so that we don't need to re-compute the generic type. + /// + public Type ResourceDefinitionType { get; set; } + + /// + /// Exposed resource attributes. + /// See https://jsonapi.org/format/#document-resource-object-attributes. + /// + public List Attributes { get; set; } + + /// + /// Exposed resource relationships. + /// See https://jsonapi.org/format/#document-resource-object-relationships + /// + public List Relationships { get; set; } + + private List _fields; + public List Fields { get { return _fields = _fields ?? Attributes.Cast().Concat(Relationships).ToList(); } } + + /// + /// Configures which links to show in the + /// object for this resource. If set to , + /// the configuration will be read from . + /// Defaults to . + /// + public Link TopLevelLinks { get; internal set; } = Link.NotConfigured; + + /// + /// Configures which links to show in the + /// object for this resource. If set to , + /// the configuration will be read from . + /// Defaults to . + /// + public Link ResourceLinks { get; internal set; } = Link.NotConfigured; + + /// + /// Configures which links to show in the + /// for all relationships of the resource for which this attribute was instantiated. + /// If set to , the configuration will + /// be read from or + /// . Defaults to . + /// + public Link RelationshipLinks { get; internal set; } = Link.NotConfigured; + + } +} diff --git a/src/JsonApiDotNetCore/Internal/ResourceGraph.cs b/src/JsonApiDotNetCore/Internal/ResourceGraph.cs index efec077536..2dc5dda57a 100644 --- a/src/JsonApiDotNetCore/Internal/ResourceGraph.cs +++ b/src/JsonApiDotNetCore/Internal/ResourceGraph.cs @@ -1,193 +1,144 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Internal { - public interface IResourceGraph - { - /// - /// Gets the value of the navigation property, defined by the relationshipName, - /// on the provided instance. - /// - /// The resource instance - /// The navigation property name. - /// - /// - /// _graph.GetRelationship(todoItem, nameof(TodoItem.Owner)); - /// - /// - /// - /// In the case of a `HasManyThrough` relationship, it will not traverse the relationship - /// and will instead return the value of the shadow property (e.g. Articles.Tags). - /// If you want to traverse the relationship, you should use . - /// - object GetRelationship(TParent resource, string propertyName); - - /// - /// Gets the value of the navigation property (defined by the ) - /// on the provided instance. - /// In the case of `HasManyThrough` relationships, it will traverse the through entity and return the - /// value of the relationship on the other side of a join entity (e.g. Articles.ArticleTags.Tag). - /// - /// The resource instance - /// The attribute used to define the relationship. - /// - /// - /// _graph.GetRelationshipValue(todoItem, nameof(TodoItem.Owner)); - /// - /// - object GetRelationshipValue(TParent resource, RelationshipAttribute relationship) where TParent : IIdentifiable; - - /// - /// Get the internal navigation property name for the specified public - /// relationship name. - /// - /// The public relationship name specified by a or - /// - /// - /// _graph.GetRelationshipName<TodoItem>("achieved-date"); - /// // returns "AchievedDate" - /// - /// - string GetRelationshipName(string relationshipName); - - /// - /// Get the resource metadata by the DbSet property name - /// - ContextEntity GetContextEntity(string dbSetName); - - /// - /// Get the resource metadata by the resource type - /// - ContextEntity GetContextEntity(Type entityType); - - /// - /// Get the public attribute name for a type based on the internal attribute name. - /// - /// The internal attribute name for a . - string GetPublicAttributeName(string internalAttributeName); - - /// - /// Helper method to get the inverse relationship attribute corresponding - /// to a relationship. - /// - RelationshipAttribute GetInverseRelationship(RelationshipAttribute relationship); - - /// - /// Was built against an EntityFrameworkCore DbContext ? - /// - bool UsesDbContext { get; } - } - + /// + /// keeps track of all the models/resources defined in JADNC + /// public class ResourceGraph : IResourceGraph { - internal List Entities { get; } internal List ValidationResults { get; } - internal static IResourceGraph Instance { get; set; } + private List _resources { get; } - public ResourceGraph() { } - public ResourceGraph(List entities, bool usesDbContext) + public ResourceGraph(List entities, List validationResults = null) { - Entities = entities; - UsesDbContext = usesDbContext; - ValidationResults = new List(); - Instance = this; - } - - // eventually, this is the planned public constructor - // to avoid breaking changes, we will be leaving the original constructor in place - // until the context graph validation process is completed - // you can track progress on this issue here: https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/170 - internal ResourceGraph(List entities, bool usesDbContext, List validationResults) - { - Entities = entities; - UsesDbContext = usesDbContext; + _resources = entities; ValidationResults = validationResults; - Instance = this; } /// - public bool UsesDbContext { get; } - + public ResourceContext[] GetResourceContexts() => _resources.ToArray(); /// - public ContextEntity GetContextEntity(string entityName) - => Entities.SingleOrDefault(e => string.Equals(e.EntityName, entityName, StringComparison.OrdinalIgnoreCase)); - + public ResourceContext GetResourceContext(string entityName) + => _resources.SingleOrDefault(e => string.Equals(e.ResourceName, entityName, StringComparison.OrdinalIgnoreCase)); /// - public ContextEntity GetContextEntity(Type entityType) - => Entities.SingleOrDefault(e => e.EntityType == entityType); - + public ResourceContext GetResourceContext(Type entityType) + => _resources.SingleOrDefault(e => e.ResourceType == entityType); /// - public object GetRelationship(TParent entity, string relationshipName) + public ResourceContext GetResourceContext() where TResource : class, IIdentifiable + => GetResourceContext(typeof(TResource)); + /// + public List GetFields(Expression> selector = null) where T : IIdentifiable { - var parentEntityType = entity.GetType(); - - var navigationProperty = parentEntityType - .GetProperties() - .SingleOrDefault(p => string.Equals(p.Name, relationshipName, StringComparison.OrdinalIgnoreCase)); - - if (navigationProperty == null) - throw new JsonApiException(400, $"{parentEntityType} does not contain a relationship named {relationshipName}"); - - return navigationProperty.GetValue(entity); + return Getter(selector).ToList(); } - - public object GetRelationshipValue(TParent resource, RelationshipAttribute relationship) where TParent : IIdentifiable + /// + public List GetAttributes(Expression> selector = null) where T : IIdentifiable { - if(relationship is HasManyThroughAttribute hasManyThroughRelationship) - { - return GetHasManyThrough(resource, hasManyThroughRelationship); - } - - return GetRelationship(resource, relationship.InternalRelationshipName); + return Getter(selector, FieldFilterType.Attribute).Cast().ToList(); } - - private IEnumerable GetHasManyThrough(IIdentifiable parent, HasManyThroughAttribute hasManyThrough) + /// + public List GetRelationships(Expression> selector = null) where T : IIdentifiable { - var throughProperty = GetRelationship(parent, hasManyThrough.InternalThroughName); - if (throughProperty is IEnumerable hasManyNavigationEntity) - { - // wrap "yield return" in a sub-function so we can correctly return null if the property is null. - return GetHasManyThroughIter(hasManyThrough, hasManyNavigationEntity); - } - return null; + return Getter(selector, FieldFilterType.Relationship).Cast().ToList(); } - - private IEnumerable GetHasManyThroughIter(HasManyThroughAttribute hasManyThrough, IEnumerable hasManyNavigationEntity) + /// + public List GetFields(Type type) { - foreach (var includedEntity in hasManyNavigationEntity) - { - var targetValue = hasManyThrough.RightProperty.GetValue(includedEntity) as IIdentifiable; - yield return targetValue; - } + return GetResourceContext(type).Fields.ToList(); + } + /// + public List GetAttributes(Type type) + { + return GetResourceContext(type).Attributes.ToList(); + } + /// + public List GetRelationships(Type type) + { + return GetResourceContext(type).Relationships.ToList(); } - /// - public string GetRelationshipName(string relationshipName) + public RelationshipAttribute GetInverse(RelationshipAttribute relationship) { - var entityType = typeof(TParent); - return Entities - .SingleOrDefault(e => e.EntityType == entityType) - ?.Relationships - .SingleOrDefault(r => r.Is(relationshipName)) - ?.InternalRelationshipName; + if (relationship.InverseNavigation == null) return null; + return GetResourceContext(relationship.RightType) + .Relationships + .SingleOrDefault(r => r.InternalRelationshipName == relationship.InverseNavigation); } - public string GetPublicAttributeName(string internalAttributeName) + private IEnumerable Getter(Expression> selector = null, FieldFilterType type = FieldFilterType.None) where T : IIdentifiable { - return GetContextEntity(typeof(TParent)) - .Attributes - .SingleOrDefault(a => a.InternalAttributeName == internalAttributeName)? - .PublicAttributeName; + IEnumerable available; + if (type == FieldFilterType.Attribute) + available = GetResourceContext(typeof(T)).Attributes.Cast(); + else if (type == FieldFilterType.Relationship) + available = GetResourceContext(typeof(T)).Relationships.Cast(); + else + available = GetResourceContext(typeof(T)).Fields; + + if (selector == null) + return available; + + var targeted = new List(); + + if (selector.Body is MemberExpression memberExpression) + { // model => model.Field1 + try + { + targeted.Add(available.Single(f => f.ExposedInternalMemberName == memberExpression.Member.Name)); + return targeted; + } + catch (InvalidOperationException) + { + ThrowNotExposedError(memberExpression.Member.Name, type); + } + } + + + if (selector.Body is NewExpression newExpression) + { // model => new { model.Field1, model.Field2 } + string memberName = null; + try + { + if (newExpression.Members == null) + return targeted; + + foreach (var member in newExpression.Members) + { + memberName = member.Name; + targeted.Add(available.Single(f => f.ExposedInternalMemberName == memberName)); + } + return targeted; + } + catch (InvalidOperationException) + { + ThrowNotExposedError(memberName, type); + } + } + + throw new ArgumentException($"The expression returned by '{selector}' for '{GetType()}' is of type {selector.Body.GetType()}" + + " and cannot be used to select resource attributes. The type must be a NewExpression.Example: article => new { article.Author };"); + } - public RelationshipAttribute GetInverseRelationship(RelationshipAttribute relationship) + private void ThrowNotExposedError(string memberName, FieldFilterType type) { - if (relationship.InverseNavigation == null) return null; - return GetContextEntity(relationship.DependentType).Relationships.SingleOrDefault(r => r.InternalRelationshipName == relationship.InverseNavigation); + throw new ArgumentException($"{memberName} is not an json:api exposed {type.ToString("g")}."); + } + + /// + /// internally used only by . + /// + private enum FieldFilterType + { + None, + Attribute, + Relationship } } } diff --git a/src/JsonApiDotNetCore/Internal/TypeHelper.cs b/src/JsonApiDotNetCore/Internal/TypeHelper.cs index fb2c7df973..e5938472c1 100644 --- a/src/JsonApiDotNetCore/Internal/TypeHelper.cs +++ b/src/JsonApiDotNetCore/Internal/TypeHelper.cs @@ -2,8 +2,8 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using System.Reflection; +using System.Linq.Expressions; using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Internal @@ -17,22 +17,28 @@ public static IList ConvertCollection(IEnumerable collection, Type targe list.Add(ConvertType(item, targetType)); return list; } - + public static bool IsNullable(Type type) + { + return (!type.IsValueType || Nullable.GetUnderlyingType(type) != null); + } public static object ConvertType(object value, Type type) { + if (value == null && !IsNullable(type)) + throw new FormatException($"Cannot convert null to a non-nullable type"); + if (value == null) return null; - var valueType = value.GetType(); + Type typeOfValue = value.GetType(); try { - if (valueType == type || type.IsAssignableFrom(valueType)) + if (typeOfValue == type || type.IsAssignableFrom(typeOfValue)) return value; type = Nullable.GetUnderlyingType(type) ?? type; - var stringValue = value.ToString(); + var stringValue = value?.ToString(); if (string.IsNullOrEmpty(stringValue)) return GetDefaultType(type); @@ -43,6 +49,7 @@ public static object ConvertType(object value, Type type) if (type == typeof(DateTimeOffset)) return DateTimeOffset.Parse(stringValue); + if (type == typeof(TimeSpan)) return TimeSpan.Parse(stringValue); @@ -53,7 +60,7 @@ public static object ConvertType(object value, Type type) } catch (Exception e) { - throw new FormatException($"{ valueType } cannot be converted to { type }", e); + throw new FormatException($"{ typeOfValue } cannot be converted to { type }", e); } } @@ -73,7 +80,7 @@ public static T ConvertType(object value) public static Type GetTypeOfList(Type type) { - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) + if (type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) { return type.GetGenericArguments()[0]; } @@ -156,9 +163,9 @@ public static Dictionary> ConvertRelat /// /// /// - public static Dictionary> ConvertAttributeDictionary(Dictionary attributes, HashSet entities) + public static Dictionary> ConvertAttributeDictionary(List attributes, HashSet entities) { - return attributes?.ToDictionary(p => p.Key.PropertyInfo, p => entities); + return attributes?.ToDictionary(attr => attr.PropertyInfo, attr => entities); } /// diff --git a/src/JsonApiDotNetCore/Internal/ValidationResults.cs b/src/JsonApiDotNetCore/Internal/ValidationResults.cs index fbaa6eb462..93fa32c74b 100644 --- a/src/JsonApiDotNetCore/Internal/ValidationResults.cs +++ b/src/JsonApiDotNetCore/Internal/ValidationResults.cs @@ -2,7 +2,7 @@ namespace JsonApiDotNetCore.Internal { - internal class ValidationResult + public class ValidationResult { public ValidationResult(LogLevel logLevel, string message) { diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index dcfe030039..b747656b40 100644 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,10 +1,10 @@  4.0.0 - $(NetStandardVersion) + $(NetCoreAppVersion) JsonApiDotNetCore JsonApiDotNetCore - 7.2 + 8.0 @@ -20,14 +20,12 @@ + - - - + - @@ -45,6 +43,6 @@ - + diff --git a/src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs b/src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs new file mode 100644 index 0000000000..0bf055cfdb --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs @@ -0,0 +1,181 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCore.Middleware +{ + /// + /// This sets all necessary parameters relating to the HttpContext for JADNC + /// + public class CurrentRequestMiddleware + { + private readonly RequestDelegate _next; + private HttpContext _httpContext; + private ICurrentRequest _currentRequest; + private IResourceGraph _resourceGraph; + private IJsonApiOptions _options; + private IControllerResourceMapping _controllerResourceMapping; + + public CurrentRequestMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext httpContext, + IControllerResourceMapping controllerResourceMapping, + IJsonApiOptions options, + ICurrentRequest currentRequest, + IResourceGraph resourceGraph) + { + _httpContext = httpContext; + _currentRequest = currentRequest; + _controllerResourceMapping = controllerResourceMapping; + _resourceGraph = resourceGraph; + _options = options; + var requestResource = GetCurrentEntity(); + if (requestResource != null) + { + _currentRequest.SetRequestResource(GetCurrentEntity()); + _currentRequest.IsRelationshipPath = PathIsRelationship(); + _currentRequest.BasePath = GetBasePath(_currentRequest.GetRequestResource().ResourceName); + } + + if (IsValid()) + { + await _next(httpContext); + } + } + + private string GetBasePath(string entityName) + { + var r = _httpContext.Request; + if (_options.RelativeLinks) + { + return GetNamespaceFromPath(r.Path, entityName); + } + return $"{r.Scheme}://{r.Host}{GetNamespaceFromPath(r.Path, entityName)}"; + } + + internal static string GetNamespaceFromPath(string path, string entityName) + { + var entityNameSpan = entityName.AsSpan(); + var pathSpan = path.AsSpan(); + const char delimiter = '/'; + for (var i = 0; i < pathSpan.Length; i++) + { + if (pathSpan[i].Equals(delimiter)) + { + var nextPosition = i + 1; + if (pathSpan.Length > i + entityNameSpan.Length) + { + var possiblePathSegment = pathSpan.Slice(nextPosition, entityNameSpan.Length); + if (entityNameSpan.SequenceEqual(possiblePathSegment)) + { + // check to see if it's the last position in the string + // or if the next character is a / + var lastCharacterPosition = nextPosition + entityNameSpan.Length; + + if (lastCharacterPosition == pathSpan.Length || pathSpan.Length >= lastCharacterPosition + 2 && pathSpan[lastCharacterPosition].Equals(delimiter)) + { + return pathSpan.Slice(0, i).ToString(); + } + } + } + } + } + + return string.Empty; + } + + protected bool PathIsRelationship() + { + var actionName = (string)_httpContext.GetRouteData().Values["action"]; + return actionName.ToLower().Contains("relationships"); + } + + private bool IsValid() + { + return IsValidContentTypeHeader(_httpContext) && IsValidAcceptHeader(_httpContext); + } + + private bool IsValidContentTypeHeader(HttpContext context) + { + var contentType = context.Request.ContentType; + if (contentType != null && ContainsMediaTypeParameters(contentType)) + { + FlushResponse(context, 415); + return false; + } + return true; + } + + private bool IsValidAcceptHeader(HttpContext context) + { + if (context.Request.Headers.TryGetValue(Constants.AcceptHeader, out StringValues acceptHeaders) == false) + return true; + + foreach (var acceptHeader in acceptHeaders) + { + if (ContainsMediaTypeParameters(acceptHeader) == false) + continue; + + FlushResponse(context, 406); + return false; + } + return true; + } + + internal static bool ContainsMediaTypeParameters(string mediaType) + { + var incomingMediaTypeSpan = mediaType.AsSpan(); + + // if the content type is not application/vnd.api+json then continue on + if (incomingMediaTypeSpan.Length < Constants.ContentType.Length) + { + return false; + } + + var incomingContentType = incomingMediaTypeSpan.Slice(0, Constants.ContentType.Length); + if (incomingContentType.SequenceEqual(Constants.ContentType.AsSpan()) == false) + return false; + + // anything appended to "application/vnd.api+json;" will be considered a media type param + return ( + incomingMediaTypeSpan.Length >= Constants.ContentType.Length + 2 + && incomingMediaTypeSpan[Constants.ContentType.Length] == ';' + ); + } + + private void FlushResponse(HttpContext context, int statusCode) + { + context.Response.StatusCode = statusCode; + context.Response.Body.Flush(); + } + + /// + /// Gets the current entity that we need for serialization and deserialization. + /// + /// + private ResourceContext GetCurrentEntity() + { + var controllerName = (string)_httpContext.GetRouteValue("controller"); + if (controllerName == null) + return null; + var resourceType = _controllerResourceMapping.GetAssociatedResource(controllerName); + var requestResource = _resourceGraph.GetResourceContext(resourceType); + if (requestResource == null) + return requestResource; + var rd = _httpContext.GetRouteData().Values; + if (rd.TryGetValue("relationshipName", out object relationshipName)) + _currentRequest.RequestRelationship = requestResource.Relationships.Single(r => r.PublicRelationshipName == (string)relationshipName); + return requestResource; + } + } +} diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs b/src/JsonApiDotNetCore/Middleware/DefaultExceptionFilter.cs similarity index 69% rename from src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs rename to src/JsonApiDotNetCore/Middleware/DefaultExceptionFilter.cs index dda1fd3b89..b6c82b27e3 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/DefaultExceptionFilter.cs @@ -1,17 +1,20 @@ -using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Middleware { - public class JsonApiExceptionFilter : ActionFilterAttribute, IExceptionFilter + /// + /// Global exception filter that wraps any thrown error with a JsonApiException. + /// + public class DefaultExceptionFilter : ActionFilterAttribute, IExceptionFilter { private readonly ILogger _logger; - public JsonApiExceptionFilter(ILoggerFactory loggerFactory) + public DefaultExceptionFilter(ILoggerFactory loggerFactory) { - _logger = loggerFactory.CreateLogger(); + _logger = loggerFactory.CreateLogger(); } public void OnException(ExceptionContext context) diff --git a/src/JsonApiDotNetCore/Middleware/TypeMatchFilter.cs b/src/JsonApiDotNetCore/Middleware/DefaultTypeMatchFilter.cs similarity index 67% rename from src/JsonApiDotNetCore/Middleware/TypeMatchFilter.cs rename to src/JsonApiDotNetCore/Middleware/DefaultTypeMatchFilter.cs index 64624dfa70..8ba43dbc26 100644 --- a/src/JsonApiDotNetCore/Middleware/TypeMatchFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/DefaultTypeMatchFilter.cs @@ -1,24 +1,24 @@ -using System; +using System; using System.Linq; using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Services; +using JsonApiDotNetCore.Internal.Contracts; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Filters; namespace JsonApiDotNetCore.Middleware { - public class TypeMatchFilter : IActionFilter + /// + /// Action filter used to verify the incoming type matches the target type, else return a 409 + /// + public class DefaultTypeMatchFilter : IActionFilter { - private readonly IJsonApiContext _jsonApiContext; + private readonly IResourceContextProvider _provider; - public TypeMatchFilter(IJsonApiContext jsonApiContext) + public DefaultTypeMatchFilter(IResourceContextProvider provider) { - _jsonApiContext = jsonApiContext; + _provider = provider; } - /// - /// Used to verify the incoming type matches the target type, else return a 409 - /// public void OnActionExecuting(ActionExecutingContext context) { var request = context.HttpContext.Request; @@ -29,11 +29,11 @@ public void OnActionExecuting(ActionExecutingContext context) if (deserializedType != null && targetType != null && deserializedType != targetType) { - var expectedJsonApiResource = _jsonApiContext.ResourceGraph.GetContextEntity(targetType); + var expectedJsonApiResource = _provider.GetResourceContext(targetType); throw new JsonApiException(409, - $"Cannot '{context.HttpContext.Request.Method}' type '{_jsonApiContext.RequestEntity.EntityName}' " - + $"to '{expectedJsonApiResource?.EntityName}' endpoint.", + $"Cannot '{context.HttpContext.Request.Method}' type '{deserializedType.Name}' " + + $"to '{expectedJsonApiResource?.ResourceName}' endpoint.", detail: "Check that the request payload type matches the type expected by this endpoint."); } } diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiExceptionFilterProvider.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiExceptionFilterProvider.cs new file mode 100644 index 0000000000..6400fa3a50 --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiExceptionFilterProvider.cs @@ -0,0 +1,20 @@ +using System; + +namespace JsonApiDotNetCore.Middleware +{ + /// + /// Provides the type of the global exception filter that is configured in MVC during startup. + /// This can be overridden to let JADNC use your own exception filter. The default exception filter used + /// is + /// + public interface IJsonApiExceptionFilterProvider + { + Type Get(); + } + + /// + public class JsonApiExceptionFilterProvider : IJsonApiExceptionFilterProvider + { + public Type Get() => typeof(DefaultExceptionFilter); + } +} diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilterProvider.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilterProvider.cs new file mode 100644 index 0000000000..50d2476890 --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilterProvider.cs @@ -0,0 +1,20 @@ +using System; + +namespace JsonApiDotNetCore.Middleware +{ + /// + /// Provides the type of the global action filter that is configured in MVC during startup. + /// This can be overridden to let JADNC use your own action filter. The default action filter used + /// is + /// + public interface IJsonApiTypeMatchFilterProvider + { + Type Get(); + } + + /// + public class JsonApiTypeMatchFilterProvider : IJsonApiTypeMatchFilterProvider + { + public Type Get() => typeof(DefaultTypeMatchFilter); + } +} diff --git a/src/JsonApiDotNetCore/Middleware/IQueryParameterActionFilter.cs b/src/JsonApiDotNetCore/Middleware/IQueryParameterActionFilter.cs new file mode 100644 index 0000000000..2c843d9d99 --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/IQueryParameterActionFilter.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace JsonApiDotNetCore.Middleware +{ + public interface IQueryParameterActionFilter + { + Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Middleware/QueryParameterFilter.cs b/src/JsonApiDotNetCore/Middleware/QueryParameterFilter.cs new file mode 100644 index 0000000000..9e66363da2 --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/QueryParameterFilter.cs @@ -0,0 +1,23 @@ +using System.Reflection; +using System.Threading.Tasks; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace JsonApiDotNetCore.Middleware +{ + public class QueryParameterActionFilter : IAsyncActionFilter, IQueryParameterActionFilter + { + private readonly IQueryParameterDiscovery _queryParser; + public QueryParameterActionFilter(IQueryParameterDiscovery queryParser) => _queryParser = queryParser; + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + // gets the DisableQueryAttribute if set on the controller that is targeted by the current request. + DisableQueryAttribute disabledQuery = context.Controller.GetType().GetTypeInfo().GetCustomAttribute(typeof(DisableQueryAttribute)) as DisableQueryAttribute; + + _queryParser.Parse(context.HttpContext.Request.Query, disabledQuery); + await next(); + } + } +} diff --git a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs deleted file mode 100644 index ab472a8dba..0000000000 --- a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Threading.Tasks; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; - -namespace JsonApiDotNetCore.Middleware -{ - public class RequestMiddleware - { - private readonly RequestDelegate _next; - - public RequestMiddleware(RequestDelegate next) - { - _next = next; - } - - public async Task Invoke(HttpContext context, IJsonApiContext jsonApiContext) - { - if (IsValid(context)) - { - // HACK: this currently results in allocation of - // objects that may or may not be used and even double allocation - // since the JsonApiContext is using field initializers - // Need to work on finding a better solution. - jsonApiContext.BeginOperation(); - await _next(context); - } - } - - private static bool IsValid(HttpContext context) - { - return IsValidContentTypeHeader(context) && IsValidAcceptHeader(context); - } - - private static bool IsValidContentTypeHeader(HttpContext context) - { - var contentType = context.Request.ContentType; - if (contentType != null && ContainsMediaTypeParameters(contentType)) - { - FlushResponse(context, 415); - return false; - } - return true; - } - - private static bool IsValidAcceptHeader(HttpContext context) - { - if (context.Request.Headers.TryGetValue(Constants.AcceptHeader, out StringValues acceptHeaders) == false) - return true; - - foreach (var acceptHeader in acceptHeaders) - { - if (ContainsMediaTypeParameters(acceptHeader) == false) - continue; - - FlushResponse(context, 406); - return false; - } - return true; - } - - internal static bool ContainsMediaTypeParameters(string mediaType) - { - var incomingMediaTypeSpan = mediaType.AsSpan(); - - // if the content type is not application/vnd.api+json then continue on - if (incomingMediaTypeSpan.Length < Constants.ContentType.Length) - return false; - - var incomingContentType = incomingMediaTypeSpan.Slice(0, Constants.ContentType.Length); - if (incomingContentType.SequenceEqual(Constants.ContentType.AsSpan()) == false) - return false; - - // anything appended to "application/vnd.api+json;" will be considered a media type param - return ( - incomingMediaTypeSpan.Length >= Constants.ContentType.Length + 2 - && incomingMediaTypeSpan[Constants.ContentType.Length] == ';' - ); - } - - private static void FlushResponse(HttpContext context, int statusCode) - { - context.Response.StatusCode = statusCode; - context.Response.Body.Flush(); - } - } -} diff --git a/src/JsonApiDotNetCore/Models/AttrAttribute.cs b/src/JsonApiDotNetCore/Models/Annotation/AttrAttribute.cs similarity index 97% rename from src/JsonApiDotNetCore/Models/AttrAttribute.cs rename to src/JsonApiDotNetCore/Models/Annotation/AttrAttribute.cs index 6d48098192..da3a9d4631 100644 --- a/src/JsonApiDotNetCore/Models/AttrAttribute.cs +++ b/src/JsonApiDotNetCore/Models/Annotation/AttrAttribute.cs @@ -4,7 +4,7 @@ namespace JsonApiDotNetCore.Models { - public class AttrAttribute : Attribute + public class AttrAttribute : Attribute, IResourceField { /// /// Defines a public attribute exposed by the API @@ -34,6 +34,9 @@ public AttrAttribute(string publicName = null, bool isImmutable = false, bool is IsSortable = isSortable; } + public string ExposedInternalMemberName => InternalAttributeName; + + /// /// Do not use this overload in your applications. /// Provides a method for instantiating instances of `AttrAttribute` and specifying diff --git a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs b/src/JsonApiDotNetCore/Models/Annotation/HasManyAttribute.cs similarity index 55% rename from src/JsonApiDotNetCore/Models/HasManyAttribute.cs rename to src/JsonApiDotNetCore/Models/Annotation/HasManyAttribute.cs index 37fe42c9af..7a05dd8fe4 100644 --- a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs +++ b/src/JsonApiDotNetCore/Models/Annotation/HasManyAttribute.cs @@ -1,4 +1,5 @@ using System; +using JsonApiDotNetCore.Models.Links; namespace JsonApiDotNetCore.Models { @@ -9,7 +10,7 @@ public class HasManyAttribute : RelationshipAttribute /// /// /// The relationship name as exposed by the API - /// Which links are available. Defaults to + /// Which links are available. Defaults to /// Whether or not this relationship can be included using the ?include=public-name query string /// The name of the entity mapped property, defaults to null /// @@ -24,24 +25,36 @@ public class HasManyAttribute : RelationshipAttribute /// /// /// - public HasManyAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string mappedBy = null, string inverseNavigationProperty = null) - : base(publicName, documentLinks, canInclude, mappedBy) + public HasManyAttribute(string publicName = null, Link relationshipLinks = Link.All, bool canInclude = true, string mappedBy = null, string inverseNavigationProperty = null) + : base(publicName, relationshipLinks, canInclude, mappedBy) { InverseNavigation = inverseNavigationProperty; } + /// + /// Gets the value of the navigation property, defined by the relationshipName, + /// on the provided instance. + /// + public override object GetValue(object entity) + { + return entity?.GetType()? + .GetProperty(InternalRelationshipName)? + .GetValue(entity); + } + + /// /// Sets the value of the property identified by this attribute /// - /// The target object + /// The target object /// The new property value - public override void SetValue(object resource, object newValue) + public override void SetValue(object entity, object newValue) { - var propertyInfo = resource + var propertyInfo = entity .GetType() .GetProperty(InternalRelationshipName); - propertyInfo.SetValue(resource, newValue); + propertyInfo.SetValue(entity, newValue); } } } diff --git a/src/JsonApiDotNetCore/Models/HasManyThroughAttribute.cs b/src/JsonApiDotNetCore/Models/Annotation/HasManyThroughAttribute.cs similarity index 68% rename from src/JsonApiDotNetCore/Models/HasManyThroughAttribute.cs rename to src/JsonApiDotNetCore/Models/Annotation/HasManyThroughAttribute.cs index c6f5bc47db..853a414b8f 100644 --- a/src/JsonApiDotNetCore/Models/HasManyThroughAttribute.cs +++ b/src/JsonApiDotNetCore/Models/Annotation/HasManyThroughAttribute.cs @@ -1,6 +1,11 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; using System.Reflection; -using System.Security; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models.Links; namespace JsonApiDotNetCore.Models { @@ -29,7 +34,7 @@ public class HasManyThroughAttribute : HasManyAttribute /// /// /// The name of the navigation property that will be used to get the HasMany relationship - /// Which links are available. Defaults to + /// Which links are available. Defaults to /// Whether or not this relationship can be included using the ?include=public-name query string /// The name of the entity mapped property, defaults to null /// @@ -38,8 +43,8 @@ public class HasManyThroughAttribute : HasManyAttribute /// [HasManyThrough(nameof(ArticleTags), documentLinks: Link.All, canInclude: true)] /// /// - public HasManyThroughAttribute(string internalThroughName, Link documentLinks = Link.All, bool canInclude = true, string mappedBy = null) - : base(null, documentLinks, canInclude, mappedBy) + public HasManyThroughAttribute(string internalThroughName, Link relationshipLinks = Link.All, bool canInclude = true, string mappedBy = null) + : base(null, relationshipLinks, canInclude, mappedBy) { InternalThroughName = internalThroughName; } @@ -65,6 +70,63 @@ public HasManyThroughAttribute(string publicName, string internalThroughName, Li InternalThroughName = internalThroughName; } + /// + /// Traverses the through the provided entity and returns the + /// value of the relationship on the other side of a join entity + /// (e.g. Articles.ArticleTags.Tag). + /// + public override object GetValue(object entity) + { + var throughNavigationProperty = entity.GetType() + .GetProperties() + .SingleOrDefault(p => string.Equals(p.Name, InternalThroughName, StringComparison.OrdinalIgnoreCase)); + + var throughEntities = throughNavigationProperty.GetValue(entity); + + if (throughEntities == null) + // return an empty list for the right-type of the property. + return TypeHelper.CreateListFor(RightType); + + // the right entities are included on the navigation/through entities. Extract and return them. + var rightEntities = new List(); + foreach (var rightEntity in (IList)throughEntities) + rightEntities.Add((IIdentifiable)RightProperty.GetValue(rightEntity)); + + return rightEntities.Cast(RightType); + } + + + /// + /// Sets the value of the property identified by this attribute + /// + /// The target object + /// The new property value + public override void SetValue(object entity, object newValue) + { + var propertyInfo = entity + .GetType() + .GetProperty(InternalRelationshipName); + propertyInfo.SetValue(entity, newValue); + + if (newValue == null) + { + ThroughProperty.SetValue(entity, null); + } + else + { + var throughRelationshipCollection = (IList)Activator.CreateInstance(ThroughProperty.PropertyType); + ThroughProperty.SetValue(entity, throughRelationshipCollection); + + foreach (IIdentifiable pointer in (IList)newValue) + { + var throughInstance = Activator.CreateInstance(ThroughType); + LeftProperty.SetValue(throughInstance, entity); + RightProperty.SetValue(throughInstance, pointer); + throughRelationshipCollection.Add(throughInstance); + } + } + } + /// /// The name of the join property on the parent resource. /// diff --git a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs b/src/JsonApiDotNetCore/Models/Annotation/HasOneAttribute.cs similarity index 69% rename from src/JsonApiDotNetCore/Models/HasOneAttribute.cs rename to src/JsonApiDotNetCore/Models/Annotation/HasOneAttribute.cs index 54a024e703..772eb32457 100644 --- a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs +++ b/src/JsonApiDotNetCore/Models/Annotation/HasOneAttribute.cs @@ -1,5 +1,5 @@ -using System; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Models.Links; namespace JsonApiDotNetCore.Models { @@ -10,7 +10,8 @@ public class HasOneAttribute : RelationshipAttribute /// /// /// The relationship name as exposed by the API - /// Which links are available. Defaults to + /// Enum to set which links should be outputted for this relationship. Defaults to which means that the configuration in + /// or is used. /// Whether or not this relationship can be included using the ?include=public-name query string /// The foreign key property name. Defaults to "{RelationshipName}Id" /// The name of the entity mapped property, defaults to null @@ -26,18 +27,25 @@ public class HasOneAttribute : RelationshipAttribute /// public int AuthorKey { get; set; } /// } /// - /// /// - public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string withForeignKey = null, string mappedBy = null, string inverseNavigationProperty = null) + public HasOneAttribute(string publicName = null, Link links = Link.NotConfigured, bool canInclude = true, string withForeignKey = null, string mappedBy = null, string inverseNavigationProperty = null) - : base(publicName, documentLinks, canInclude, mappedBy) + : base(publicName, links, canInclude, mappedBy) { _explicitIdentifiablePropertyName = withForeignKey; InverseNavigation = inverseNavigationProperty; } + + public override object GetValue(object entity) + { + return entity?.GetType()? + .GetProperty(InternalRelationshipName)? + .GetValue(entity); + } + private readonly string _explicitIdentifiablePropertyName; - + /// /// The independent resource identifier. /// @@ -48,18 +56,23 @@ public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, /// /// Sets the value of the property identified by this attribute /// - /// The target object + /// The target object /// The new property value - public override void SetValue(object resource, object newValue) + public override void SetValue(object entity, object newValue) { string propertyName = InternalRelationshipName; // if we're deleting the relationship (setting it to null), // we set the foreignKey to null. We could also set the actual property to null, // but then we would first need to load the current relationship, which requires an extra query. if (newValue == null) propertyName = IdentifiablePropertyName; - - var propertyInfo = resource.GetType().GetProperty(propertyName); - propertyInfo.SetValue(resource, newValue); + var resourceType = entity.GetType(); + var propertyInfo = resourceType.GetProperty(propertyName); + if (propertyInfo == null) + { + // we can't set the FK to null because there isn't any. + propertyInfo = resourceType.GetProperty(RelationshipPath); + } + propertyInfo.SetValue(entity, newValue); } // HACK: this will likely require boxing diff --git a/src/JsonApiDotNetCore/Models/Annotation/IRelationshipField.cs b/src/JsonApiDotNetCore/Models/Annotation/IRelationshipField.cs new file mode 100644 index 0000000000..a20d5fd00d --- /dev/null +++ b/src/JsonApiDotNetCore/Models/Annotation/IRelationshipField.cs @@ -0,0 +1,6 @@ +namespace JsonApiDotNetCore.Models +{ + public interface IRelationshipField + { + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Models/Annotation/IResourceField.cs b/src/JsonApiDotNetCore/Models/Annotation/IResourceField.cs new file mode 100644 index 0000000000..90ec306c93 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/Annotation/IResourceField.cs @@ -0,0 +1,7 @@ +namespace JsonApiDotNetCore.Models +{ + public interface IResourceField + { + string ExposedInternalMemberName { get; } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Models/Annotation/LinksAttribute.cs b/src/JsonApiDotNetCore/Models/Annotation/LinksAttribute.cs new file mode 100644 index 0000000000..1c071723f1 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/Annotation/LinksAttribute.cs @@ -0,0 +1,43 @@ +using System; +using JsonApiDotNetCore.Internal; + +namespace JsonApiDotNetCore.Models.Links +{ + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public class LinksAttribute : Attribute + { + public LinksAttribute(Link topLevelLinks = Link.NotConfigured, Link resourceLinks = Link.NotConfigured, Link relationshipLinks = Link.NotConfigured) + { + if (topLevelLinks == Link.Related) + throw new JsonApiSetupException($"{Link.Related.ToString("g")} not allowed for argument {nameof(topLevelLinks)}"); + + if (resourceLinks == Link.Paging) + throw new JsonApiSetupException($"{Link.Paging.ToString("g")} not allowed for argument {nameof(resourceLinks)}"); + + if (relationshipLinks == Link.Paging) + throw new JsonApiSetupException($"{Link.Paging.ToString("g")} not allowed for argument {nameof(relationshipLinks)}"); + + TopLevelLinks = topLevelLinks; + ResourceLinks = resourceLinks; + RelationshipLinks = relationshipLinks; + } + + /// + /// Configures which links to show in the + /// object for this resource. + /// + public Link TopLevelLinks { get; private set; } + + /// + /// Configures which links to show in the + /// object for this resource. + /// + public Link ResourceLinks { get; private set; } + + /// + /// Configures which links to show in the + /// for all relationships of the resource for which this attribute was instantiated. + /// + public Link RelationshipLinks { get; private set; } + } +} diff --git a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Models/Annotation/RelationshipAttribute.cs similarity index 55% rename from src/JsonApiDotNetCore/Models/RelationshipAttribute.cs rename to src/JsonApiDotNetCore/Models/Annotation/RelationshipAttribute.cs index 3bd44f5030..0e92e5ea84 100644 --- a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Models/Annotation/RelationshipAttribute.cs @@ -1,19 +1,24 @@ using System; -using System.Runtime.CompilerServices; using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models.Links; namespace JsonApiDotNetCore.Models { - public abstract class RelationshipAttribute : Attribute + public abstract class RelationshipAttribute : Attribute, IResourceField, IRelationshipField { - protected RelationshipAttribute(string publicName, Link documentLinks, bool canInclude, string mappedBy) + protected RelationshipAttribute(string publicName, Link relationshipLinks, bool canInclude, string mappedBy) { + if (relationshipLinks == Link.Paging) + throw new JsonApiSetupException($"{Link.Paging.ToString("g")} not allowed for argument {nameof(relationshipLinks)}"); + PublicRelationshipName = publicName; - DocumentLinks = documentLinks; + RelationshipLinks = relationshipLinks; CanInclude = canInclude; EntityPropertyName = mappedBy; } + public string ExposedInternalMemberName => InternalRelationshipName; public string PublicRelationshipName { get; internal set; } public string InternalRelationshipName { get; internal set; } public string InverseNavigation { get; internal set; } @@ -28,62 +33,27 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI /// public List<Tag> Tags { get; set; } // Type => Tag /// /// - [Obsolete("Use property DependentType")] - public Type Type { get { return DependentType; } internal set { DependentType = value; } } + public Type RightType { get; internal set; } /// - /// The related entity type. This does not necessarily match the navigation property type. - /// In the case of a HasMany relationship, this value will be the generic argument type. - /// - /// The technical language as used in EF Core is used here (dependent vs principal). + /// The parent entity type. This is the type of the class in which this attribute was used. /// - /// - /// - /// - /// public List<Tag> Tags { get; set; } // Type => Tag - /// - /// - public Type DependentType { get; internal set; } - - /// - /// The parent entity type. The technical language as used in EF Core is used here (dependent vs principal). - /// - public Type PrincipalType { get; internal set; } + public Type LeftType { get; internal set; } public bool IsHasMany => GetType() == typeof(HasManyAttribute) || GetType().Inherits(typeof(HasManyAttribute)); public bool IsHasOne => GetType() == typeof(HasOneAttribute); - public Link DocumentLinks { get; } = Link.All; + + /// + /// Configures which links to show in the + /// object for this relationship. + /// + public Link RelationshipLinks { get; } public bool CanInclude { get; } public string EntityPropertyName { get; } - public bool TryGetHasOne(out HasOneAttribute result) - { - if (IsHasOne) - { - result = (HasOneAttribute)this; - return true; - } - result = null; - return false; - } - - public bool TryGetHasMany(out HasManyAttribute result) - { - if (IsHasMany) - { - result = (HasManyAttribute)this; - return true; - } - result = null; - return false; - } - public abstract void SetValue(object entity, object newValue); - - public object GetValue(object entity) => entity - ?.GetType()? - .GetProperty(InternalRelationshipName)? - .GetValue(entity); + + public abstract object GetValue(object entity); public override string ToString() { @@ -98,12 +68,7 @@ public override bool Equals(object obj) } bool equalRelationshipName = PublicRelationshipName.Equals(attr.PublicRelationshipName); - bool equalPrincipalType = true; - if (PrincipalType != null) - { - equalPrincipalType = PrincipalType.Equals(attr.PrincipalType); - } - return IsHasMany == attr.IsHasMany && equalRelationshipName && equalPrincipalType; + return IsHasMany == attr.IsHasMany && equalRelationshipName; } /// diff --git a/src/JsonApiDotNetCore/Models/Document.cs b/src/JsonApiDotNetCore/Models/Document.cs deleted file mode 100644 index 5d0d10d188..0000000000 --- a/src/JsonApiDotNetCore/Models/Document.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Models -{ - public class Document : DocumentBase - { - [JsonProperty("data")] - public ResourceObject Data { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Models/DocumentBase.cs b/src/JsonApiDotNetCore/Models/DocumentBase.cs deleted file mode 100644 index 8812d301e5..0000000000 --- a/src/JsonApiDotNetCore/Models/DocumentBase.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Models -{ - public class DocumentBase - { - [JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)] - public RootLinks Links { get; set; } - - [JsonProperty("included", NullValueHandling = NullValueHandling.Ignore)] - public List Included { get; set; } - - [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] - public Dictionary Meta { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Models/Documents.cs b/src/JsonApiDotNetCore/Models/Documents.cs deleted file mode 100644 index 8e1dcbb36e..0000000000 --- a/src/JsonApiDotNetCore/Models/Documents.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Models -{ - public class Documents : DocumentBase - { - [JsonProperty("data")] - public List Data { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Models/IHasMeta.cs b/src/JsonApiDotNetCore/Models/IHasMeta.cs index 50d86e6034..f1605bf790 100644 --- a/src/JsonApiDotNetCore/Models/IHasMeta.cs +++ b/src/JsonApiDotNetCore/Models/IHasMeta.cs @@ -1,10 +1,9 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Services; namespace JsonApiDotNetCore.Models { public interface IHasMeta { - Dictionary GetMeta(IJsonApiContext context); + Dictionary GetMeta(); } } diff --git a/src/JsonApiDotNetCore/Models/IResourceMapper.cs b/src/JsonApiDotNetCore/Models/IResourceMapper.cs deleted file mode 100644 index 94034f715b..0000000000 --- a/src/JsonApiDotNetCore/Models/IResourceMapper.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace JsonApiDotNetCore.Models -{ - public interface IResourceMapper - { - /// - /// Execute a mapping from the source object to a new destination object. - /// The source type is inferred from the source object. - /// - /// Destination type to create - /// Source object to map from - /// Mapped destination object - TDestination Map(object source); - - /// - /// Execute a mapping from the source object to a new destination object. - /// - /// Source type to use, regardless of the runtime type - /// Destination type to create - /// Source object to map from - /// Mapped destination object - TDestination Map(TSource source); - } -} diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/Document.cs b/src/JsonApiDotNetCore/Models/JsonApiDocuments/Document.cs new file mode 100644 index 0000000000..0b74574ee9 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/JsonApiDocuments/Document.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models.Links; +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.Models +{ + /// + /// https://jsonapi.org/format/#document-structure + /// + public class Document : ExposableData + { + /// + /// see "meta" in https://jsonapi.org/format/#document-top-level + /// + [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] + public Dictionary Meta { get; set; } + + /// + /// see "links" in https://jsonapi.org/format/#document-top-level + /// + [JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)] + public TopLevelLinks Links { get; set; } + + /// + /// see "included" in https://jsonapi.org/format/#document-top-level + /// + [JsonProperty("included", NullValueHandling = NullValueHandling.Ignore, Order = 1)] + public List Included { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ExposableData.cs b/src/JsonApiDotNetCore/Models/JsonApiDocuments/ExposableData.cs new file mode 100644 index 0000000000..bb2f9a2800 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/JsonApiDocuments/ExposableData.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace JsonApiDotNetCore.Models +{ + public class ExposableData where T : class + { + /// + /// see "primary data" in https://jsonapi.org/format/#document-top-level. + /// + [JsonProperty("data")] + public object Data { get { return GetPrimaryData(); } set { SetPrimaryData(value); } } + + /// + /// see https://www.newtonsoft.com/json/help/html/ConditionalProperties.htm + /// + /// + /// Moving this method to the derived class where it is needed only in the + /// case of would make more sense, but + /// Newtonsoft does not support this. + /// + public bool ShouldSerializeData() + { + if (GetType() == typeof(RelationshipEntry)) + return IsPopulated; + return true; + } + + /// + /// Internally used for "single" primary data. + /// + internal T SingleData { get; private set; } + + /// + /// Internally used for "many" primary data. + /// + internal List ManyData { get; private set; } + + /// + /// Internally used to indicate if the document's primary data is + /// "single" or "many". + /// + internal bool IsManyData { get; private set; } = false; + + /// + /// Internally used to indicate if the document's primary data is + /// should still be serialized when it's value is null. This is used when + /// a single resource is requested but not present (eg /articles/1/author). + /// + internal bool IsPopulated { get; private set; } = false; + + internal bool HasResource { get { return IsPopulated && ((IsManyData && ManyData.Any()) || SingleData != null); } } + + /// + /// Gets the "single" or "many" data depending on which one was + /// assigned in this document. + /// + protected object GetPrimaryData() + { + if (IsManyData) + return ManyData; + return SingleData; + } + + /// + /// Sets the primary data depending on if it is "single" or "many" data. + /// + protected void SetPrimaryData(object value) + { + IsPopulated = true; + if (value is JObject jObject) + SingleData = jObject.ToObject(); + else if (value is T ro) + SingleData = ro; + else if (value != null) + { + IsManyData = true; + if (value is JArray jArray) + ManyData = jArray.ToObject>(); + else + ManyData = (List)value; + } + } + } +} diff --git a/src/JsonApiDotNetCore/Models/IIdentifiable.cs b/src/JsonApiDotNetCore/Models/JsonApiDocuments/IIdentifiable.cs similarity index 100% rename from src/JsonApiDotNetCore/Models/IIdentifiable.cs rename to src/JsonApiDotNetCore/Models/JsonApiDocuments/IIdentifiable.cs diff --git a/src/JsonApiDotNetCore/Models/Identifiable.cs b/src/JsonApiDotNetCore/Models/JsonApiDocuments/Identifiable.cs similarity index 87% rename from src/JsonApiDotNetCore/Models/Identifiable.cs rename to src/JsonApiDotNetCore/Models/JsonApiDocuments/Identifiable.cs index b62f31fe89..b85128444e 100644 --- a/src/JsonApiDotNetCore/Models/Identifiable.cs +++ b/src/JsonApiDotNetCore/Models/JsonApiDocuments/Identifiable.cs @@ -38,7 +38,7 @@ public string StringId protected virtual string GetStringId(object value) { if(value == null) - return string.Empty; + return string.Empty; // todo; investigate why not using null, because null would make more sense in serialization var type = typeof(T); var stringValue = value.ToString(); @@ -59,8 +59,9 @@ protected virtual string GetStringId(object value) /// protected virtual T GetTypedId(string value) { - var convertedValue = TypeHelper.ConvertType(value, typeof(T)); - return convertedValue == null ? default : (T)convertedValue; + if (value == null) + return default; + return (T)TypeHelper.ConvertType(value, typeof(T)); } } } diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/Link.cs b/src/JsonApiDotNetCore/Models/JsonApiDocuments/Link.cs new file mode 100644 index 0000000000..5bba6273a0 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/JsonApiDocuments/Link.cs @@ -0,0 +1,15 @@ +using System; + +namespace JsonApiDotNetCore.Models.Links +{ + [Flags] + public enum Link + { + Self = 1 << 0, + Related = 1 << 1, + Paging = 1 << 2, + NotConfigured = 1 << 3, + None = 1 << 4, + All = Self | Related | Paging + } +} diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/RelationshipEntry.cs b/src/JsonApiDotNetCore/Models/JsonApiDocuments/RelationshipEntry.cs new file mode 100644 index 0000000000..d0c718b721 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/JsonApiDocuments/RelationshipEntry.cs @@ -0,0 +1,11 @@ +using JsonApiDotNetCore.Models.Links; +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.Models +{ + public class RelationshipEntry : ExposableData + { + [JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)] + public RelationshipLinks Links { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/RelationshipLinks.cs b/src/JsonApiDotNetCore/Models/JsonApiDocuments/RelationshipLinks.cs new file mode 100644 index 0000000000..c728df6777 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/JsonApiDocuments/RelationshipLinks.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.Models.Links +{ + public class RelationshipLinks + { + /// + /// see "links" bulletin at https://jsonapi.org/format/#document-resource-object-relationships + /// + [JsonProperty("self", NullValueHandling = NullValueHandling.Ignore)] + public string Self { get; set; } + + /// + /// https://jsonapi.org/format/#document-resource-object-related-resource-links + /// + [JsonProperty("related", NullValueHandling = NullValueHandling.Ignore)] + public string Related { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Models/ResourceIdentifierObject.cs b/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceIdentifierObject.cs similarity index 64% rename from src/JsonApiDotNetCore/Models/ResourceIdentifierObject.cs rename to src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceIdentifierObject.cs index 2cf4ef401e..939cae0820 100644 --- a/src/JsonApiDotNetCore/Models/ResourceIdentifierObject.cs +++ b/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceIdentifierObject.cs @@ -11,13 +11,12 @@ public ResourceIdentifierObject(string type, string id) Id = id; } - [JsonProperty("type")] + [JsonProperty("type", Order = -3)] public string Type { get; set; } - [JsonProperty("id")] + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore, Order = -2)] public string Id { get; set; } - [JsonProperty("lid")] - public string LocalId { get; set; } + public override string ToString() => $"(type: {Type}, id: {Id})"; } } diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceLinks.cs b/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceLinks.cs new file mode 100644 index 0000000000..ea701f7681 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceLinks.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.Models.Links +{ + public class ResourceLinks + { + /// + /// https://jsonapi.org/format/#document-resource-object-links + /// + [JsonProperty("self", NullValueHandling = NullValueHandling.Ignore)] + public string Self { get; set; } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceObject.cs b/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceObject.cs new file mode 100644 index 0000000000..55dc096adb --- /dev/null +++ b/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceObject.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models.Links; +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.Models +{ + public class ResourceObject : ResourceIdentifierObject + { + [JsonProperty("attributes", NullValueHandling = NullValueHandling.Ignore)] + public Dictionary Attributes { get; set; } + + [JsonProperty("relationships", NullValueHandling = NullValueHandling.Ignore)] + public Dictionary Relationships { get; set; } + + [JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)] + public ResourceLinks Links { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceObjectComparer.cs b/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceObjectComparer.cs new file mode 100644 index 0000000000..e7da59bb98 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceObjectComparer.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Builders +{ + class ResourceObjectComparer : IEqualityComparer + { + public bool Equals(ResourceObject x, ResourceObject y) + { + return x.Id.Equals(y.Id) && x.Type.Equals(y.Type); + } + + public int GetHashCode(ResourceObject ro) + { + return ro.GetHashCode(); + } + } +} diff --git a/src/JsonApiDotNetCore/Models/RootLinks.cs b/src/JsonApiDotNetCore/Models/JsonApiDocuments/TopLevelLinks.cs similarity index 85% rename from src/JsonApiDotNetCore/Models/RootLinks.cs rename to src/JsonApiDotNetCore/Models/JsonApiDocuments/TopLevelLinks.cs index 42b0a7863f..22c8d12f16 100644 --- a/src/JsonApiDotNetCore/Models/RootLinks.cs +++ b/src/JsonApiDotNetCore/Models/JsonApiDocuments/TopLevelLinks.cs @@ -1,8 +1,11 @@ using Newtonsoft.Json; -namespace JsonApiDotNetCore.Models +namespace JsonApiDotNetCore.Models.Links { - public class RootLinks + /// + /// see links section in https://jsonapi.org/format/#document-top-level + /// + public class TopLevelLinks { [JsonProperty("self")] public string Self { get; set; } diff --git a/src/JsonApiDotNetCore/Models/Link.cs b/src/JsonApiDotNetCore/Models/Link.cs deleted file mode 100644 index 2d99fa7197..0000000000 --- a/src/JsonApiDotNetCore/Models/Link.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace JsonApiDotNetCore.Models -{ - [Flags] - public enum Link - { - Self = 1 << 0, - Paging = 1 << 1, - Related = 1 << 2, - All = ~(-1 << 3), - None = 1 << 4, - } -} diff --git a/src/JsonApiDotNetCore/Models/Links.cs b/src/JsonApiDotNetCore/Models/Links.cs deleted file mode 100644 index 993ca209d0..0000000000 --- a/src/JsonApiDotNetCore/Models/Links.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Models -{ - public class Links - { - [JsonProperty("self")] - public string Self { get; set; } - - [JsonProperty("related")] - public string Related { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Models/LinksAttribute.cs b/src/JsonApiDotNetCore/Models/LinksAttribute.cs deleted file mode 100644 index 85e2693111..0000000000 --- a/src/JsonApiDotNetCore/Models/LinksAttribute.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace JsonApiDotNetCore.Models -{ - public class LinksAttribute : Attribute - { - public LinksAttribute(Link links) - { - Links = links; - } - - public Link Links { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Models/Operations/Operation.cs b/src/JsonApiDotNetCore/Models/Operations/Operation.cs deleted file mode 100644 index 604643d231..0000000000 --- a/src/JsonApiDotNetCore/Models/Operations/Operation.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Linq; - -namespace JsonApiDotNetCore.Models.Operations -{ - public class Operation : DocumentBase - { - [JsonProperty("op"), JsonConverter(typeof(StringEnumConverter))] - public OperationCode Op { get; set; } - - [JsonProperty("ref", NullValueHandling = NullValueHandling.Ignore)] - public ResourceReference Ref { get; set; } - - [JsonProperty("params", NullValueHandling = NullValueHandling.Ignore)] - public Params Params { get; set; } - - [JsonProperty("data")] - public object Data - { - get - { - if (DataIsList) return DataList; - return DataObject; - } - set => SetData(value); - } - - private void SetData(object data) - { - if (data is JArray jArray) - { - DataIsList = true; - DataList = jArray.ToObject>(); - } - else if (data is List dataList) - { - DataIsList = true; - DataList = dataList; - } - else if (data is JObject jObject) - { - DataObject = jObject.ToObject(); - } - else if (data is ResourceObject dataObject) - { - DataObject = dataObject; - } - } - - [JsonIgnore] - public bool DataIsList { get; private set; } - - [JsonIgnore] - public List DataList { get; private set; } - - [JsonIgnore] - public ResourceObject DataObject { get; private set; } - - public string GetResourceTypeName() - { - if (Ref != null) - return Ref.Type?.ToString(); - - if (DataIsList) - return DataList[0].Type?.ToString(); - - return DataObject.Type?.ToString(); - } - } -} diff --git a/src/JsonApiDotNetCore/Models/Operations/OperationCode.cs b/src/JsonApiDotNetCore/Models/Operations/OperationCode.cs deleted file mode 100644 index 6b6905cd59..0000000000 --- a/src/JsonApiDotNetCore/Models/Operations/OperationCode.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; - -namespace JsonApiDotNetCore.Models.Operations -{ - [JsonConverter(typeof(StringEnumConverter))] - public enum OperationCode - { - get = 1, - add = 2, - update = 3, - remove = 4 - } -} diff --git a/src/JsonApiDotNetCore/Models/Operations/OperationsDocument.cs b/src/JsonApiDotNetCore/Models/Operations/OperationsDocument.cs deleted file mode 100644 index 3228e9ca88..0000000000 --- a/src/JsonApiDotNetCore/Models/Operations/OperationsDocument.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Models.Operations -{ - public class OperationsDocument - { - public OperationsDocument() { } - public OperationsDocument(List operations) - { - Operations = operations; - } - - [JsonProperty("operations")] - public List Operations { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Models/Operations/Params.cs b/src/JsonApiDotNetCore/Models/Operations/Params.cs deleted file mode 100644 index 470e8f4aa3..0000000000 --- a/src/JsonApiDotNetCore/Models/Operations/Params.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; - -namespace JsonApiDotNetCore.Models.Operations -{ - public class Params - { - public List Include { get; set; } - public List Sort { get; set; } - public Dictionary Filter { get; set; } - public string Page { get; set; } - public Dictionary Fields { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Models/Operations/ResourceReference.cs b/src/JsonApiDotNetCore/Models/Operations/ResourceReference.cs deleted file mode 100644 index 5291d61a19..0000000000 --- a/src/JsonApiDotNetCore/Models/Operations/ResourceReference.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Models.Operations -{ - public class ResourceReference : ResourceIdentifierObject - { - [JsonProperty("relationship")] - public string Relationship { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Models/RelationshipData.cs b/src/JsonApiDotNetCore/Models/RelationshipData.cs deleted file mode 100644 index 1cfe47c5c7..0000000000 --- a/src/JsonApiDotNetCore/Models/RelationshipData.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace JsonApiDotNetCore.Models -{ - public class RelationshipData - { - [JsonProperty("links")] - public Links Links { get; set; } - - [JsonProperty("data")] - public object ExposedData - { - get - { - if (ManyData != null) - return ManyData; - return SingleData; - } - set - { - if (value is JObject jObject) - SingleData = jObject.ToObject(); - else if (value is ResourceIdentifierObject dict) - SingleData = (ResourceIdentifierObject)value; - else - SetManyData(value); - } - } - - private void SetManyData(object value) - { - IsHasMany = true; - if (value is JArray jArray) - ManyData = jArray.ToObject>(); - else - ManyData = (List)value; - } - - [JsonIgnore] - public List ManyData { get; set; } - - [JsonIgnore] - public ResourceIdentifierObject SingleData { get; set; } - - [JsonIgnore] - public bool IsHasMany { get; private set; } - } -} diff --git a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs index 2939bc40d1..6f7798e484 100644 --- a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs @@ -1,130 +1,57 @@ using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Hooks; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Reflection; namespace JsonApiDotNetCore.Models { - public interface IResourceDefinition { - List GetOutputAttrs(object instance); + List GetAllowedAttributes(); + List GetAllowedRelationships(); + object GetCustomQueryFilter(string key); + List<(AttrAttribute, SortDirection)> DefaultSort(); } - /// /// exposes developer friendly hooks into how their resources are exposed. /// It is intended to improve the experience and reduce boilerplate for commonly required features. /// The goal of this class is to reduce the frequency with which developers have to override the /// service and repository layers. /// - /// The resource type - public class ResourceDefinition : IResourceDefinition, IResourceHookContainer where T : class, IIdentifiable + /// The resource type + public class ResourceDefinition : IResourceDefinition, IResourceHookContainer where TResource : class, IIdentifiable { - private readonly ContextEntity _contextEntity; - internal readonly bool _instanceAttrsAreSpecified; - - private bool _requestCachedAttrsHaveBeenLoaded = false; - private List _requestCachedAttrs; - - public ResourceDefinition(IResourceGraph graph) + private readonly ResourceContext _resourceContext; + private readonly IResourceGraph _resourceGraph; + private List _allowedAttributes; + private List _allowedRelationships; + public ResourceDefinition(IResourceGraph resourceGraph) { - _contextEntity = graph.GetContextEntity(typeof(T)); - _instanceAttrsAreSpecified = InstanceOutputAttrsAreSpecified(); + _resourceContext = resourceGraph.GetResourceContext(typeof(TResource)); + _allowedAttributes = _resourceContext.Attributes; + _allowedRelationships = _resourceContext.Relationships; + _resourceGraph = resourceGraph; } - private bool InstanceOutputAttrsAreSpecified() - { - var derivedType = GetType(); - var methods = derivedType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance); - var instanceMethod = methods - .Where(m => - m.Name == nameof(OutputAttrs) - && m.GetParameters() - .FirstOrDefault() - ?.ParameterType == typeof(T)) - .FirstOrDefault(); - var declaringType = instanceMethod?.DeclaringType; - return declaringType == derivedType; - } - - /// - /// Remove an attribute - /// - /// the filter to execute - /// @TODO - /// - protected List Remove(Expression> filter, List from = null) - { - //@TODO: need to investigate options for caching these - from = from ?? _contextEntity.Attributes; - - // model => model.Attribute - if (filter.Body is MemberExpression memberExpression) - return _contextEntity.Attributes - .Where(a => a.InternalAttributeName != memberExpression.Member.Name) - .ToList(); - - // model => new { model.Attribute1, model.Attribute2 } - if (filter.Body is NewExpression newExpression) - { - var attributes = new List(); - foreach (var attr in _contextEntity.Attributes) - if (newExpression.Members.Any(m => m.Name == attr.InternalAttributeName) == false) - attributes.Add(attr); - - return attributes; - } - - throw new JsonApiException(500, - message: $"The expression returned by '{filter}' for '{GetType()}' is of type {filter.Body.GetType()}" - + " and cannot be used to select resource attributes. ", - detail: "The type must be a NewExpression. Example: article => new { article.Author }; "); - } - /// - /// Allows POST / PATCH requests to set the value of an - /// attribute, but exclude the attribute in the response - /// this might be used if the incoming value gets hashed or - /// encrypted prior to being persisted and this value should - /// never be sent back to the client. - /// - /// Called once per filtered resource in request. - /// - protected virtual List OutputAttrs() => _contextEntity.Attributes; + public List GetAllowedRelationships() => _allowedRelationships; + public List GetAllowedAttributes() => _allowedAttributes; /// - /// Allows POST / PATCH requests to set the value of an - /// attribute, but exclude the attribute in the response - /// this might be used if the incoming value gets hashed or - /// encrypted prior to being persisted and this value should - /// never be sent back to the client. - /// - /// Called for every instance of a resource. + /// Hides specified attributes and relationships from the serialized output. Can be called directly in a resource definition implementation or + /// in any resource hook to combine it with eg authorization. /// - protected virtual List OutputAttrs(T instance) => _contextEntity.Attributes; - - public List GetOutputAttrs(object instance) - => _instanceAttrsAreSpecified == false - ? GetOutputAttrs() - : OutputAttrs(instance as T); - - private List GetOutputAttrs() + /// Should be of the form: (TResource e) => new { e.Attribute1, e.Arttribute2, e.Relationship1, e.Relationship2 } + public void HideFields(Expression> selector) { - if (_requestCachedAttrsHaveBeenLoaded == false) - { - _requestCachedAttrs = OutputAttrs(); - // the reason we don't just check for null is because we - // guarantee that OutputAttrs will be called once per - // request and null is a valid return value - _requestCachedAttrsHaveBeenLoaded = true; - } - - return _requestCachedAttrs; + var fieldsToHide = _resourceGraph.GetFields(selector); + _allowedAttributes = _allowedAttributes.Except(fieldsToHide.Where(f => f is AttrAttribute)).Cast().ToList(); + _allowedRelationships = _allowedRelationships.Except(fieldsToHide.Where(f => f is RelationshipAttribute)).Cast().ToList(); } /// @@ -164,30 +91,38 @@ private List GetOutputAttrs() /// public virtual QueryFilters GetQueryFilters() => null; + public object GetCustomQueryFilter(string key) + { + var customFilters = GetQueryFilters(); + if (customFilters != null && customFilters.TryGetValue(key, out var query)) + return query; + return null; + } + /// - public virtual void AfterCreate(HashSet entities, ResourcePipeline pipeline) { } + public virtual void AfterCreate(HashSet entities, ResourcePipeline pipeline) { } /// - public virtual void AfterRead(HashSet entities, ResourcePipeline pipeline, bool isIncluded = false) { } + public virtual void AfterRead(HashSet entities, ResourcePipeline pipeline, bool isIncluded = false) { } /// - public virtual void AfterUpdate(HashSet entities, ResourcePipeline pipeline) { } + public virtual void AfterUpdate(HashSet entities, ResourcePipeline pipeline) { } /// - public virtual void AfterDelete(HashSet entities, ResourcePipeline pipeline, bool succeeded) { } + public virtual void AfterDelete(HashSet entities, ResourcePipeline pipeline, bool succeeded) { } /// - public virtual void AfterUpdateRelationship(IRelationshipsDictionary entitiesByRelationship, ResourcePipeline pipeline) { } + public virtual void AfterUpdateRelationship(IRelationshipsDictionary entitiesByRelationship, ResourcePipeline pipeline) { } /// - public virtual IEnumerable BeforeCreate(IEntityHashSet entities, ResourcePipeline pipeline) { return entities; } + public virtual IEnumerable BeforeCreate(IEntityHashSet entities, ResourcePipeline pipeline) { return entities; } /// public virtual void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) { } /// - public virtual IEnumerable BeforeUpdate(IDiffableEntityHashSet entities, ResourcePipeline pipeline) { return entities; } + public virtual IEnumerable BeforeUpdate(IDiffableEntityHashSet entities, ResourcePipeline pipeline) { return entities; } /// - public virtual IEnumerable BeforeDelete(IEntityHashSet entities, ResourcePipeline pipeline) { return entities; } + public virtual IEnumerable BeforeDelete(IEntityHashSet entities, ResourcePipeline pipeline) { return entities; } /// - public virtual IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary entitiesByRelationship, ResourcePipeline pipeline) { return ids; } + public virtual IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary entitiesByRelationship, ResourcePipeline pipeline) { return ids; } /// - public virtual void BeforeImplicitUpdateRelationship(IRelationshipsDictionary entitiesByRelationship, ResourcePipeline pipeline) { } + public virtual void BeforeImplicitUpdateRelationship(IRelationshipsDictionary entitiesByRelationship, ResourcePipeline pipeline) { } /// - public virtual IEnumerable OnReturn(HashSet entities, ResourcePipeline pipeline) { return entities; } + public virtual IEnumerable OnReturn(HashSet entities, ResourcePipeline pipeline) { return entities; } /// @@ -195,7 +130,7 @@ public virtual void BeforeImplicitUpdateRelationship(IRelationshipsDictionary /// method signature. /// See for usage details. /// - public class QueryFilters : Dictionary, FilterQuery, IQueryable>> { } + public class QueryFilters : Dictionary, FilterQuery, IQueryable>> { } /// /// Define a the default sort order if no sort key is provided. @@ -221,14 +156,7 @@ public class QueryFilters : Dictionary, FilterQuery, { var order = new List<(AttrAttribute, SortDirection)>(); foreach (var sortProp in defaultSortOrder) - { - // TODO: error handling, log or throw? - if (sortProp.Item1.Body is MemberExpression memberExpression) - order.Add( - (_contextEntity.Attributes.SingleOrDefault(a => a.InternalAttributeName != memberExpression.Member.Name), - sortProp.Item2) - ); - } + order.Add((_resourceGraph.GetAttributes(sortProp.Item1).Single(), sortProp.Item2)); return order; } @@ -241,6 +169,6 @@ public class QueryFilters : Dictionary, FilterQuery, /// method signature. /// See for usage details. /// - public class PropertySortOrder : List<(Expression>, SortDirection)> { } + public class PropertySortOrder : List<(Expression>, SortDirection)> { } } } diff --git a/src/JsonApiDotNetCore/Models/ResourceObject.cs b/src/JsonApiDotNetCore/Models/ResourceObject.cs deleted file mode 100644 index 1a28631407..0000000000 --- a/src/JsonApiDotNetCore/Models/ResourceObject.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Models -{ - public class ResourceObject : ResourceIdentifierObject - { - [JsonProperty("attributes")] - public Dictionary Attributes { get; set; } - - [JsonProperty("relationships", NullValueHandling = NullValueHandling.Ignore)] - public Dictionary Relationships { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs b/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..7c21e6f218 --- /dev/null +++ b/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("IntegrationTests")] diff --git a/src/Examples/OperationsExample/Properties/launchSettings.json b/src/JsonApiDotNetCore/Properties/launchSettings.json similarity index 76% rename from src/Examples/OperationsExample/Properties/launchSettings.json rename to src/JsonApiDotNetCore/Properties/launchSettings.json index b0d8e5bd4b..d0f3094262 100644 --- a/src/Examples/OperationsExample/Properties/launchSettings.json +++ b/src/JsonApiDotNetCore/Properties/launchSettings.json @@ -1,27 +1,27 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:53656/", - "sslPort": 0 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "OperationsExample": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:53657/" - } - } +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:63521/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "JsonApiDotNetCore": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:63522/" + } + } } \ No newline at end of file diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Common/IQueryParameterParser.cs b/src/JsonApiDotNetCore/QueryParameterServices/Common/IQueryParameterParser.cs new file mode 100644 index 0000000000..69163e37f4 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/Common/IQueryParameterParser.cs @@ -0,0 +1,14 @@ +using JsonApiDotNetCore.Controllers; +using Microsoft.AspNetCore.Http; + +namespace JsonApiDotNetCore.Services +{ + /// + /// Responsible for populating the various service implementations of + /// . + /// + public interface IQueryParameterDiscovery + { + void Parse(IQueryCollection query, DisableQueryAttribute disabledQuery = null); + } +} diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Common/IQueryParameterService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Common/IQueryParameterService.cs new file mode 100644 index 0000000000..64df236abc --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/Common/IQueryParameterService.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCore.Query +{ + /// + /// Base interface that all query parameter services should inherit. + /// + public interface IQueryParameterService + { + /// + /// Parses the value of the query parameter. Invoked in the middleware. + /// + /// the value of the query parameter as retrieved from the url + void Parse(KeyValuePair queryParameter); + /// + /// The name of the query parameter as matched in the URL query string. + /// + string Name { get; } + } +} diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Common/QueryParameterParser.cs b/src/JsonApiDotNetCore/QueryParameterServices/Common/QueryParameterParser.cs new file mode 100644 index 0000000000..1e397afbf7 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/Common/QueryParameterParser.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Query; +using Microsoft.AspNetCore.Http; + +namespace JsonApiDotNetCore.Services +{ + /// + public class QueryParameterDiscovery : IQueryParameterDiscovery + { + private readonly IJsonApiOptions _options; + private readonly IEnumerable _queryServices; + + public QueryParameterDiscovery(IJsonApiOptions options, IEnumerable queryServices) + { + _options = options; + _queryServices = queryServices; + } + + /// + /// For a query parameter in , calls + /// the + /// method of the corresponding service. + /// + public virtual void Parse(IQueryCollection query, DisableQueryAttribute disabled) + { + var disabledQuery = disabled?.QueryParams; + + foreach (var pair in query) + { + bool parsed = false; + foreach (var service in _queryServices) + { + if (pair.Key.ToLower().StartsWith(service.Name, StringComparison.Ordinal)) + { + if (disabledQuery == null || !IsDisabled(disabledQuery, service)) + service.Parse(pair); + parsed = true; + break; + } + } + if (parsed) + continue; + + if (!_options.AllowCustomQueryParameters) + throw new JsonApiException(400, $"{pair} is not a valid query."); + } + } + + private bool IsDisabled(string disabledQuery, IQueryParameterService targetsService) + { + if (disabledQuery == QueryParams.All.ToString("G").ToLower()) + return true; + + if (disabledQuery == targetsService.Name) + return true; + + return false; + } + } +} diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Common/QueryParameterService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Common/QueryParameterService.cs new file mode 100644 index 0000000000..cadd91f6fd --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/Common/QueryParameterService.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCore.Query +{ + /// + /// Base clas for query parameters. + /// + public abstract class QueryParameterService + { + protected readonly IResourceGraph _resourceGraph; + protected readonly ResourceContext _requestResource; + + protected QueryParameterService(IResourceGraph resourceGraph, ICurrentRequest currentRequest) + { + _resourceGraph = resourceGraph; + _requestResource = currentRequest.GetRequestResource(); + } + + protected QueryParameterService() { } + + /// + /// Derives the name of the query parameter from the name of the implementing type. + /// + /// + /// The following query param service will match the query displayed in URL + /// `?include=some-relationship` + /// public class IncludeService : QueryParameterService { /* ... */ } + /// + public virtual string Name { get { return GetParameterNameFromType(); } } + + /// + /// Gets the query parameter name from the implementing class name. Trims "Service" + /// from the name if present. + /// + private string GetParameterNameFromType() => new Regex("Service$").Replace(GetType().Name, string.Empty).ToLower(); + + /// + /// Helper method for parsing query parameters into attributes + /// + protected AttrAttribute GetAttribute(string target, RelationshipAttribute relationship = null) + { + AttrAttribute attribute; + if (relationship != null) + attribute = _resourceGraph.GetAttributes(relationship.RightType).FirstOrDefault(a => a.Is(target)); + else + attribute = _requestResource.Attributes.FirstOrDefault(attr => attr.Is(target)); + + if (attribute == null) + throw new JsonApiException(400, $"'{target}' is not a valid attribute."); + + return attribute; + } + + /// + /// Helper method for parsing query parameters into relationships attributes + /// + protected RelationshipAttribute GetRelationship(string propertyName) + { + if (propertyName == null) return null; + var relationship = _requestResource.Relationships.FirstOrDefault(r => r.Is(propertyName)); + if (relationship == null) + throw new JsonApiException(400, $"{propertyName} is not a valid relationship on {_requestResource.ResourceName}."); + + return relationship; + } + } +} diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IFilterService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IFilterService.cs new file mode 100644 index 0000000000..02e4d623e8 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IFilterService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Internal.Query; + +namespace JsonApiDotNetCore.Query +{ + /// + /// Query parameter service responsible for url queries of the form ?filter[X]=Y + /// + public interface IFilterService : IQueryParameterService + { + /// + /// Gets the parsed filter queries + /// + List Get(); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IIncludeService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IIncludeService.cs new file mode 100644 index 0000000000..0de79a3d17 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IIncludeService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Query +{ + /// + /// Query parameter service responsible for url queries of the form ?include=X.Y.Z,U.V.W + /// + public interface IIncludeService : IQueryParameterService + { + /// + /// Gets the parsed relationship inclusion chains. + /// + List> Get(); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitDefaultService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitDefaultService.cs new file mode 100644 index 0000000000..eab6399407 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitDefaultService.cs @@ -0,0 +1,13 @@ +namespace JsonApiDotNetCore.Query +{ + /// + /// Query parameter service responsible for url queries of the form ?omitDefault=true + /// + public interface IOmitDefaultService : IQueryParameterService + { + /// + /// Gets the parsed config + /// + bool Config { get; } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitNullService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitNullService.cs new file mode 100644 index 0000000000..519d8add42 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitNullService.cs @@ -0,0 +1,13 @@ +namespace JsonApiDotNetCore.Query +{ + /// + /// Query parameter service responsible for url queries of the form ?omitNull=true + /// + public interface IOmitNullService : IQueryParameterService + { + /// + /// Gets the parsed config + /// + bool Config { get; } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IPageService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IPageService.cs new file mode 100644 index 0000000000..76f56baf6a --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IPageService.cs @@ -0,0 +1,35 @@ +namespace JsonApiDotNetCore.Query +{ + /// + /// Query parameter service responsible for url queries of the form ?page[size]=X&page[number]=Y + /// + public interface IPageService : IQueryParameterService + { + /// + /// What the total records are for this output + /// + int? TotalRecords { get; set; } + /// + /// How many records per page should be shown + /// + int PageSize { get; set; } + /// + /// What is the default page size + /// + int DefaultPageSize { get; set; } + /// + /// What page are we currently on + /// + int CurrentPage { get; set; } + + /// + /// Total amount of pages for request + /// + int TotalPages { get; } + + /// + /// Checks if pagination is enabled + /// + bool ShouldPaginate(); + } +} diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/ISortService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/ISortService.cs new file mode 100644 index 0000000000..781da03713 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/ISortService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Internal.Query; + +namespace JsonApiDotNetCore.Query +{ + /// + /// Query parameter service responsible for url queries of the form ?sort=-X + /// + public interface ISortService : IQueryParameterService + { + /// + /// Gets the parsed sort queries + /// + List Get(); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/ISparseFieldsService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/ISparseFieldsService.cs new file mode 100644 index 0000000000..a5879d7595 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/ISparseFieldsService.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Query +{ + /// + /// Query parameter service responsible for url queries of the form ?fields[X]=U,V,W + /// + public interface ISparseFieldsService : IQueryParameterService + { + /// + /// Gets the list of targeted fields. If a relationship is supplied, + /// gets the list of targeted fields for that relationship. + /// + /// + List Get(RelationshipAttribute relationship = null); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/QueryParameterServices/FilterService.cs b/src/JsonApiDotNetCore/QueryParameterServices/FilterService.cs new file mode 100644 index 0000000000..feed8c49fe --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/FilterService.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCore.Query +{ + /// + public class FilterService : QueryParameterService, IFilterService + { + private readonly List _filters; + private IResourceDefinition _requestResourceDefinition; + + public FilterService(IResourceDefinitionProvider resourceDefinitionProvider, IResourceGraph resourceGraph, ICurrentRequest currentRequest) : base(resourceGraph, currentRequest) + { + _requestResourceDefinition = resourceDefinitionProvider.Get(_requestResource.ResourceType); + _filters = new List(); + } + + /// + public List Get() + { + return _filters; + } + + /// + public virtual void Parse(KeyValuePair queryParameter) + { + var queries = GetFilterQueries(queryParameter); + _filters.AddRange(queries.Select(GetQueryContexts)); + } + + private FilterQueryContext GetQueryContexts(FilterQuery query) + { + var queryContext = new FilterQueryContext(query); + if (_requestResourceDefinition != null) + { + var customQuery = _requestResourceDefinition.GetCustomQueryFilter(query.Target); + if (customQuery != null) + { + queryContext.IsCustom = true; + queryContext.CustomQuery = customQuery; + return queryContext; + } + } + + queryContext.Relationship = GetRelationship(query.Relationship); + var attribute = GetAttribute(query.Attribute, queryContext.Relationship); + + if (attribute.IsFilterable == false) + throw new JsonApiException(400, $"Filter is not allowed for attribute '{attribute.PublicAttributeName}'."); + queryContext.Attribute = attribute; + + return queryContext; + } + + /// todo: this could be simplified a bunch + private List GetFilterQueries(KeyValuePair queryParameter) + { + // expected input = filter[id]=1 + // expected input = filter[id]=eq:1 + var propertyName = queryParameter.Key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; + var queries = new List(); + // InArray case + string op = GetFilterOperation(queryParameter.Value); + if (string.Equals(op, FilterOperation.@in.ToString(), StringComparison.OrdinalIgnoreCase) + || string.Equals(op, FilterOperation.nin.ToString(), StringComparison.OrdinalIgnoreCase)) + { + (var _, var filterValue) = ParseFilterOperation(queryParameter.Value); + queries.Add(new FilterQuery(propertyName, filterValue, op)); + } + else + { + var values = ((string)queryParameter.Value).Split(QueryConstants.COMMA); + foreach (var val in values) + { + (var operation, var filterValue) = ParseFilterOperation(val); + queries.Add(new FilterQuery(propertyName, filterValue, operation)); + } + } + return queries; + } + + /// todo: this could be simplified a bunch + private (string operation, string value) ParseFilterOperation(string value) + { + if (value.Length < 3) + return (string.Empty, value); + + var operation = GetFilterOperation(value); + var values = value.Split(QueryConstants.COLON); + + if (string.IsNullOrEmpty(operation)) + return (string.Empty, value); + + value = string.Join(QueryConstants.COLON_STR, values.Skip(1)); + + return (operation, value); + } + + /// todo: this could be simplified a bunch + private string GetFilterOperation(string value) + { + var values = value.Split(QueryConstants.COLON); + + if (values.Length == 1) + return string.Empty; + + var operation = values[0]; + // remove prefix from value + if (Enum.TryParse(operation, out FilterOperation op) == false) + return string.Empty; + + return operation; + } + } +} diff --git a/src/JsonApiDotNetCore/QueryParameterServices/IncludeService.cs b/src/JsonApiDotNetCore/QueryParameterServices/IncludeService.cs new file mode 100644 index 0000000000..4032da4c53 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/IncludeService.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCore.Query +{ + public class IncludeService : QueryParameterService, IIncludeService + { + /// todo: use read-only lists. + private readonly List> _includedChains; + + public IncludeService(IResourceGraph resourceGraph, ICurrentRequest currentRequest) : base(resourceGraph, currentRequest) + { + _includedChains = new List>(); + } + + /// + public List> Get() + { + return _includedChains.Select(chain => chain.ToList()).ToList(); + } + + /// + public virtual void Parse(KeyValuePair queryParameter) + { + var value = (string)queryParameter.Value; + if (string.IsNullOrWhiteSpace(value)) + throw new JsonApiException(400, "Include parameter must not be empty if provided"); + + var chains = value.Split(QueryConstants.COMMA).ToList(); + foreach (var chain in chains) + ParseChain(chain); + } + + private void ParseChain(string chain) + { + var parsedChain = new List(); + var chainParts = chain.Split(QueryConstants.DOT); + var resourceContext = _requestResource; + foreach (var relationshipName in chainParts) + { + var relationship = resourceContext.Relationships.SingleOrDefault(r => r.PublicRelationshipName == relationshipName); + if (relationship == null) + throw InvalidRelationshipError(resourceContext, relationshipName); + + if (relationship.CanInclude == false) + throw CannotIncludeError(resourceContext, relationshipName); + + parsedChain.Add(relationship); + resourceContext = _resourceGraph.GetResourceContext(relationship.RightType); + } + _includedChains.Add(parsedChain); + } + + private JsonApiException CannotIncludeError(ResourceContext resourceContext, string requestedRelationship) + { + return new JsonApiException(400, $"Including the relationship {requestedRelationship} on {resourceContext.ResourceName} is not allowed"); + } + + private JsonApiException InvalidRelationshipError(ResourceContext resourceContext, string requestedRelationship) + { + return new JsonApiException(400, $"Invalid relationship {requestedRelationship} on {resourceContext.ResourceName}", + $"{resourceContext.ResourceName} does not have a relationship named {requestedRelationship}"); + } + } +} diff --git a/src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs b/src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs new file mode 100644 index 0000000000..0887f414b0 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCore.Query +{ + /// + public class OmitDefaultService : QueryParameterService, IOmitDefaultService + { + private readonly IJsonApiOptions _options; + + public OmitDefaultService(IJsonApiOptions options) + { + Config = options.DefaultAttributeResponseBehavior.OmitDefaultValuedAttributes; + _options = options; + } + + /// + public bool Config { get; private set; } + + /// + public virtual void Parse(KeyValuePair queryParameter) + { + if (!_options.DefaultAttributeResponseBehavior.AllowClientOverride) + return; + + if (!bool.TryParse(queryParameter.Value, out var config)) + return; + + Config = config; + } + } +} diff --git a/src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs b/src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs new file mode 100644 index 0000000000..57d69866af --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCore.Query +{ + /// + public class OmitNullService : QueryParameterService, IOmitNullService + { + private readonly IJsonApiOptions _options; + + public OmitNullService(IJsonApiOptions options) + { + Config = options.NullAttributeResponseBehavior.OmitNullValuedAttributes; + _options = options; + } + + /// + public bool Config { get; private set; } + + /// + public virtual void Parse(KeyValuePair queryParameter) + { + if (!_options.NullAttributeResponseBehavior.AllowClientOverride) + return; + + if (!bool.TryParse(queryParameter.Value, out var config)) + return; + + Config = config; + } + } +} diff --git a/src/JsonApiDotNetCore/QueryParameterServices/PageService.cs b/src/JsonApiDotNetCore/QueryParameterServices/PageService.cs new file mode 100644 index 0000000000..d4aa5052ee --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/PageService.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Query; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCore.Query +{ + /// + public class PageService : QueryParameterService, IPageService + { + private IJsonApiOptions _options; + + public PageService(IJsonApiOptions options) + { + _options = options; + DefaultPageSize = _options.DefaultPageSize; + PageSize = _options.DefaultPageSize; + } + /// + public int? TotalRecords { get; set; } + /// + public int PageSize { get; set; } + /// + public int DefaultPageSize { get; set; } // I think we shouldnt expose this + /// + public int CurrentPage { get; set; } + /// + public int TotalPages => (TotalRecords == null) ? -1 : (int)Math.Ceiling(decimal.Divide(TotalRecords.Value, PageSize)); + + /// + public virtual void Parse(KeyValuePair queryParameter) + { + // expected input = page[size]=10 + // page[number]=1 + var propertyName = queryParameter.Key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; + + const string SIZE = "size"; + const string NUMBER = "number"; + + if (propertyName == SIZE) + { + if (int.TryParse(queryParameter.Value, out var size)) + PageSize = size; + else + throw new JsonApiException(400, $"Invalid page size '{queryParameter.Value}'"); + } + else if (propertyName == NUMBER) + { + if (int.TryParse(queryParameter.Value, out var size)) + CurrentPage = size; + else + throw new JsonApiException(400, $"Invalid page number '{queryParameter.Value}'"); + } + } + + /// + public bool ShouldPaginate() + { + return (PageSize > 0) || ((CurrentPage == 1 || CurrentPage == 0) && TotalPages <= 0); + } + } +} diff --git a/src/JsonApiDotNetCore/QueryParameterServices/SortService.cs b/src/JsonApiDotNetCore/QueryParameterServices/SortService.cs new file mode 100644 index 0000000000..cd5a283e07 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/SortService.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCore.Query +{ + /// + public class SortService : QueryParameterService, ISortService + { + const char DESCENDING_SORT_OPERATOR = '-'; + private readonly IResourceDefinitionProvider _resourceDefinitionProvider; + private List _queries; + private bool _isProcessed; + + public SortService(IResourceDefinitionProvider resourceDefinitionProvider, + IResourceGraph resourceGraph, + ICurrentRequest currentRequest) + : base(resourceGraph, currentRequest) + { + _resourceDefinitionProvider = resourceDefinitionProvider; + _queries = new List(); + } + + /// + public virtual void Parse(KeyValuePair queryParameter) + { + CheckIfProcessed(); // disallow multiple sort parameters. + var queries = BuildQueries(queryParameter.Value); + + _queries = queries.Select(BuildQueryContext).ToList(); + } + + /// + public List Get() + { + if (_queries == null) + { + var requestResourceDefinition = _resourceDefinitionProvider.Get(_requestResource.ResourceType); + if (requestResourceDefinition != null) + return requestResourceDefinition.DefaultSort()?.Select(d => BuildQueryContext(new SortQuery(d.Item1.PublicAttributeName, d.Item2))).ToList(); + } + return _queries.ToList(); + } + + private List BuildQueries(string value) + { + var sortParameters = new List(); + + var sortSegments = value.Split(QueryConstants.COMMA); + if (sortSegments.Any(s => s == string.Empty)) + throw new JsonApiException(400, "The sort URI segment contained a null value."); + + foreach (var sortSegment in sortSegments) + { + var propertyName = sortSegment; + var direction = SortDirection.Ascending; + + if (sortSegment[0] == DESCENDING_SORT_OPERATOR) + { + direction = SortDirection.Descending; + propertyName = propertyName.Substring(1); + } + + sortParameters.Add(new SortQuery(propertyName, direction)); + } + + return sortParameters; + } + + private SortQueryContext BuildQueryContext(SortQuery query) + { + var relationship = GetRelationship(query.Relationship); + var attribute = GetAttribute(query.Attribute, relationship); + + if (attribute.IsSortable == false) + throw new JsonApiException(400, $"Sort is not allowed for attribute '{attribute.PublicAttributeName}'."); + + return new SortQueryContext(query) + { + Attribute = attribute, + Relationship = relationship + }; + } + + private void CheckIfProcessed() + { + if (_isProcessed) + throw new JsonApiException(400, "The sort query parameter occured in the URI more than once."); + + _isProcessed = true; + } + + } +} diff --git a/src/JsonApiDotNetCore/QueryParameterServices/SparseFieldsService.cs b/src/JsonApiDotNetCore/QueryParameterServices/SparseFieldsService.cs new file mode 100644 index 0000000000..83b5fcdff3 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/SparseFieldsService.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCore.Query +{ + /// + public class SparseFieldsService : QueryParameterService, ISparseFieldsService + { + /// + /// The selected fields for the primary resource of this request. + /// + private List _selectedFields; + /// + /// The selected field for any included relationships + /// + private readonly Dictionary> _selectedRelationshipFields; + + public override string Name => "fields"; + + public SparseFieldsService(IResourceGraph resourceGraph, ICurrentRequest currentRequest) : base(resourceGraph, currentRequest) + { + _selectedFields = new List(); + _selectedRelationshipFields = new Dictionary>(); + } + + /// + public List Get(RelationshipAttribute relationship = null) + { + if (relationship == null) + return _selectedFields; + + _selectedRelationshipFields.TryGetValue(relationship, out var fields); + return fields; + } + + /// + public virtual void Parse(KeyValuePair queryParameter) + { // expected: articles?fields=prop1,prop2 + // articles?fields[articles]=prop1,prop2 <-- this form in invalid UNLESS "articles" is actually a relationship on Article + // articles?fields[relationship]=prop1,prop2 + var fields = new List { nameof(Identifiable.Id) }; + fields.AddRange(((string)queryParameter.Value).Split(QueryConstants.COMMA)); + + var keySplitted = queryParameter.Key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET); + + if (keySplitted.Count() == 1) + { // input format: fields=prop1,prop2 + foreach (var field in fields) + RegisterRequestResourceField(field); + } + else + { // input format: fields[articles]=prop1,prop2 + string navigation = keySplitted[1]; + // it is possible that the request resource has a relationship + // that is equal to the resource name, like with self-referering data types (eg directory structures) + // if not, no longer support this type of sparse field selection. + if (navigation == _requestResource.ResourceName && !_requestResource.Relationships.Any(a => a.Is(navigation))) + throw new JsonApiException(400, $"Use '?fields=...' instead of 'fields[{navigation}]':" + + $" the square bracket navigations is now reserved " + + $"for relationships only. See https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/555#issuecomment-543100865"); + + if (navigation.Contains(QueryConstants.DOT)) + throw new JsonApiException(400, $"fields[{navigation}] is not valid: deeply nested sparse field selection is not yet supported."); + + var relationship = _requestResource.Relationships.SingleOrDefault(a => a.Is(navigation)); + if (relationship == null) + throw new JsonApiException(400, $"'{navigation}' in 'fields[{navigation}]' is not a valid relationship of {_requestResource.ResourceName}"); + + foreach (var field in fields) + RegisterRelatedResourceField(field, relationship); + } + } + + /// + /// Registers field selection queries of the form articles?fields[author]=first-name + /// + private void RegisterRelatedResourceField(string field, RelationshipAttribute relationship) + { + var relationProperty = _resourceGraph.GetResourceContext(relationship.RightType); + var attr = relationProperty.Attributes.SingleOrDefault(a => a.Is(field)); + if (attr == null) + throw new JsonApiException(400, $"'{relationship.RightType.Name}' does not contain '{field}'."); + + if (!_selectedRelationshipFields.TryGetValue(relationship, out var registeredFields)) + _selectedRelationshipFields.Add(relationship, registeredFields = new List()); + registeredFields.Add(attr); + } + + /// + /// Registers field selection queries of the form articles?fields=title + /// + private void RegisterRequestResourceField(string field) + { + var attr = _requestResource.Attributes.SingleOrDefault(a => a.Is(field)); + if (attr == null) + throw new JsonApiException(400, $"'{_requestResource.ResourceName}' does not contain '{field}'."); + + (_selectedFields = _selectedFields ?? new List()).Add(attr); + } + } +} diff --git a/src/JsonApiDotNetCore/Request/HasManyRelationshipPointers.cs b/src/JsonApiDotNetCore/Request/HasManyRelationshipPointers.cs deleted file mode 100644 index 3fda5dc44e..0000000000 --- a/src/JsonApiDotNetCore/Request/HasManyRelationshipPointers.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCore.Request -{ - /// - /// Stores information to set relationships for the request resource. - /// These relationships must already exist and should not be re-created. - /// - /// The expected use case is POST-ing or PATCH-ing - /// an entity with HasMany relaitonships: - /// - /// { - /// "data": { - /// "type": "photos", - /// "attributes": { - /// "title": "Ember Hamster", - /// "src": "http://example.com/images/productivity.png" - /// }, - /// "relationships": { - /// "tags": { - /// "data": [ - /// { "type": "tags", "id": "2" }, - /// { "type": "tags", "id": "3" } - /// ] - /// } - /// } - /// } - /// } - /// - /// - public class HasManyRelationshipPointers - { - private readonly Dictionary _hasManyRelationships = new Dictionary(); - - /// - /// Add the relationship to the list of relationships that should be - /// set in the repository layer. - /// - public void Add(RelationshipAttribute relationship, IList entities) - => _hasManyRelationships[relationship] = entities; - - /// - /// Get all the models that should be associated - /// - public Dictionary Get() => _hasManyRelationships; - } -} diff --git a/src/JsonApiDotNetCore/Request/HasOneRelationshipPointers.cs b/src/JsonApiDotNetCore/Request/HasOneRelationshipPointers.cs deleted file mode 100644 index 19046b9eaa..0000000000 --- a/src/JsonApiDotNetCore/Request/HasOneRelationshipPointers.cs +++ /dev/null @@ -1,45 +0,0 @@ -using JsonApiDotNetCore.Models; -using System.Collections.Generic; - -namespace JsonApiDotNetCore.Request -{ - /// - /// Stores information to set relationships for the request resource. - /// These relationships must already exist and should not be re-created. - /// - /// The expected use case is POST-ing or PATCH-ing - /// an entity with HasOne relationships: - /// - /// { - /// "data": { - /// "type": "photos", - /// "attributes": { - /// "title": "Ember Hamster", - /// "src": "http://example.com/images/productivity.png" - /// }, - /// "relationships": { - /// "photographer": { - /// "data": { "type": "people", "id": "2" } - /// } - /// } - /// } - /// } - /// - /// - public class HasOneRelationshipPointers - { - private readonly Dictionary _hasOneRelationships = new Dictionary(); - - /// - /// Add the relationship to the list of relationships that should be - /// set in the repository layer. - /// - public void Add(HasOneAttribute relationship, IIdentifiable entity) - => _hasOneRelationships[relationship] = entity; - - /// - /// Get all the models that should be associated - /// - public Dictionary Get() => _hasOneRelationships; - } -} diff --git a/src/JsonApiDotNetCore/RequestServices/Contracts/ICurrentRequest.cs b/src/JsonApiDotNetCore/RequestServices/Contracts/ICurrentRequest.cs new file mode 100644 index 0000000000..0260db8c91 --- /dev/null +++ b/src/JsonApiDotNetCore/RequestServices/Contracts/ICurrentRequest.cs @@ -0,0 +1,41 @@ +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Managers.Contracts +{ + /// + /// Metadata associated to the current json:api request. + /// + public interface ICurrentRequest + { + /// + /// The request namespace. This may be an absolute or relative path + /// depending upon the configuration. + /// + /// + /// Absolute: https://example.com/api/v1 + /// + /// Relative: /api/v1 + /// + string BasePath { get; set; } + + /// + /// If the request is on the `{id}/relationships/{relationshipName}` route + /// + bool IsRelationshipPath { get; set; } + + /// + /// If is true, this property + /// is the relationship attribute associated with the targeted relationship + /// + RelationshipAttribute RequestRelationship { get; set; } + + /// + /// Sets the current context entity for this entire request + /// + /// + void SetRequestResource(ResourceContext currentResourceContext); + + ResourceContext GetRequestResource(); + } +} diff --git a/src/JsonApiDotNetCore/RequestServices/Contracts/IUpdatedFields.cs b/src/JsonApiDotNetCore/RequestServices/Contracts/IUpdatedFields.cs new file mode 100644 index 0000000000..fe6e2ffc33 --- /dev/null +++ b/src/JsonApiDotNetCore/RequestServices/Contracts/IUpdatedFields.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Serialization +{ + /// + /// Container to register which attributes and relationships are targeted by the current operation. + /// + public interface ITargetedFields + { + /// + /// List of attributes that are updated by a request + /// + List Attributes { get; set; } + /// + /// List of relationships that are updated by a request + /// + List Relationships { get; set; } + } + +} diff --git a/src/JsonApiDotNetCore/RequestServices/CurrentRequest.cs b/src/JsonApiDotNetCore/RequestServices/CurrentRequest.cs new file mode 100644 index 0000000000..abb1b5863a --- /dev/null +++ b/src/JsonApiDotNetCore/RequestServices/CurrentRequest.cs @@ -0,0 +1,28 @@ +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Managers +{ + class CurrentRequest : ICurrentRequest + { + private ResourceContext _resourceContext; + public string BasePath { get; set; } + public bool IsRelationshipPath { get; set; } + public RelationshipAttribute RequestRelationship { get; set; } + + /// + /// The main resource of the request. + /// + /// + public ResourceContext GetRequestResource() + { + return _resourceContext; + } + + public void SetRequestResource(ResourceContext primaryResource) + { + _resourceContext = primaryResource; + } + } +} diff --git a/src/JsonApiDotNetCore/RequestServices/TargetedFields.cs b/src/JsonApiDotNetCore/RequestServices/TargetedFields.cs new file mode 100644 index 0000000000..b5a4ee18d8 --- /dev/null +++ b/src/JsonApiDotNetCore/RequestServices/TargetedFields.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Serialization +{ + /// + public class TargetedFields : ITargetedFields + { + /// + public List Attributes { get; set; } = new List(); + /// + public List Relationships { get; set; } = new List(); + } + +} diff --git a/src/JsonApiDotNetCore/Serialization/Client/DeserializedResponse.cs b/src/JsonApiDotNetCore/Serialization/Client/DeserializedResponse.cs new file mode 100644 index 0000000000..179e62a34f --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Client/DeserializedResponse.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.Links; + +namespace JsonApiDotNetCore.Serialization.Client +{ + /// Base class for "single data" and "many data" deserialized responses. + /// TODO: Currently and + /// information is ignored by the serializer. This is out of scope for now because + /// it is not considered mission critical for v4. + public abstract class DeserializedResponseBase + { + public TopLevelLinks Links { get; set; } + public Dictionary Meta { get; set; } + public object Errors { get; set; } + public object JsonApi { get; set; } + } + + /// + /// Represents a deserialized document with "single data". + /// + /// Type of the resource in the primary data + public class DeserializedSingleResponse : DeserializedResponseBase where TResource : class, IIdentifiable + { + public TResource Data { get; set; } + } + + /// + /// Represents a deserialized document with "many data". + /// + /// Type of the resource(s) in the primary data + public class DeserializedListResponse : DeserializedResponseBase where TResource : class, IIdentifiable + { + public List Data { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Client/IRequestSerializer.cs b/src/JsonApiDotNetCore/Serialization/Client/IRequestSerializer.cs new file mode 100644 index 0000000000..168eb1e393 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Client/IRequestSerializer.cs @@ -0,0 +1,41 @@ +using System.Collections; +using System.Linq.Expressions; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Serialization.Client +{ + /// + /// Interface for client serializer that can be used to register with the DI, for usage in + /// custom services or repositories. + /// + public interface IRequestSerializer + { + /// + /// Creates and serializes a document for a single intance of a resource. + /// + /// Entity to serialize + /// The serialized content + string Serialize(IIdentifiable entity); + /// + /// Creates and serializes a document for for a list of entities of one resource. + /// + /// Entities to serialize + /// The serialized content + string Serialize(IEnumerable entities); + /// + /// Sets the s to serialize for resources of type . + /// If no s are specified, by default all attributes are included in the serialized result. + /// + /// Type of the resource to serialize + /// Should be of the form: (TResource e) => new { e.Attr1, e.Attr2 } + void SetAttributesToSerialize(Expression> filter) where TResource : class, IIdentifiable; + /// + /// Sets the s to serialize for resources of type . + /// If no s are specified, by default no relationships are included in the serialization result. + /// The should be of the form: (TResource e) => new { e.Attr1, e.Attr2 } + /// + /// Type of the resource to serialize + /// Should be of the form: (TResource e) => new { e.Attr1, e.Attr2 } + void SetRelationshipsToSerialize(Expression> filter) where TResource : class, IIdentifiable; + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/Client/IResponseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Client/IResponseDeserializer.cs new file mode 100644 index 0000000000..3cd4497c15 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Client/IResponseDeserializer.cs @@ -0,0 +1,26 @@ +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Serialization.Client +{ + /// + /// Client deserializer. Currently not used internally in JsonApiDotNetCore, + /// except for in the tests. Exposed pubically to make testing easier or to implement + /// server-to-server communication. + /// + public interface IResponseDeserializer + { + /// + /// Deserializes a response with a single resource (or null) as data. + /// + /// The type of the resources in the primary data + /// The JSON to be deserialized + DeserializedSingleResponse DeserializeSingle(string body) where TResource : class, IIdentifiable; + + /// + /// Deserializes a response with a (empty) list of resources as data. + /// + /// The type of the resources in the primary data + /// The JSON to be deserialized + DeserializedListResponse DeserializeList(string body) where TResource : class, IIdentifiable; + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/Client/RequestSerializer.cs b/src/JsonApiDotNetCore/Serialization/Client/RequestSerializer.cs new file mode 100644 index 0000000000..7f5d827dbe --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Client/RequestSerializer.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq.Expressions; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.Serialization.Client +{ + /// + /// Client serializer implementation of + /// + public class RequestSerializer : BaseDocumentBuilder, IRequestSerializer + { + private readonly Dictionary> _attributesToSerializeCache; + private readonly Dictionary> _relationshipsToSerializeCache; + private Type _currentTargetedResource; + private readonly IResourceGraph _resourceGraph; + public RequestSerializer(IResourceGraph resourceGraph, + IResourceObjectBuilder resourceObjectBuilder) + : base(resourceObjectBuilder, resourceGraph) + { + _resourceGraph = resourceGraph; + _attributesToSerializeCache = new Dictionary>(); + _relationshipsToSerializeCache = new Dictionary>(); + } + + /// + public string Serialize(IIdentifiable entity) + { + if (entity == null) + return JsonConvert.SerializeObject(Build(entity, new List(), new List())); + + _currentTargetedResource = entity?.GetType(); + var document = Build(entity, GetAttributesToSerialize(entity), GetRelationshipsToSerialize(entity)); + _currentTargetedResource = null; + return JsonConvert.SerializeObject(document); + } + + /// + public string Serialize(IEnumerable entities) + { + IIdentifiable entity = null; + foreach (IIdentifiable item in entities) + { + entity = item; + break; + } + if (entity == null) + return JsonConvert.SerializeObject(Build(entities, new List(), new List())); + + _currentTargetedResource = entity?.GetType(); + var attributes = GetAttributesToSerialize(entity); + var relationships = GetRelationshipsToSerialize(entity); + var document = base.Build(entities, attributes, relationships); + _currentTargetedResource = null; + return JsonConvert.SerializeObject(document); + } + + /// + public void SetAttributesToSerialize(Expression> filter) + where TResource : class, IIdentifiable + { + var allowedAttributes = _resourceGraph.GetAttributes(filter); + _attributesToSerializeCache[typeof(TResource)] = allowedAttributes; + } + + /// + public void SetRelationshipsToSerialize(Expression> filter) + where TResource : class, IIdentifiable + { + var allowedRelationships = _resourceGraph.GetRelationships(filter); + _relationshipsToSerializeCache[typeof(TResource)] = allowedRelationships; + } + + /// + /// By default, the client serializer includes all attributes in the result, + /// unless a list of allowed attributes was supplied using the + /// method. For any related resources, attributes are never exposed. + /// + private List GetAttributesToSerialize(IIdentifiable entity) + { + var resourceType = entity.GetType(); + if (_currentTargetedResource != resourceType) + // We're dealing with a relationship that is being serialized, for which + // we never want to include any attributes in the payload. + return new List(); + + if (!_attributesToSerializeCache.TryGetValue(resourceType, out var attributes)) + return _resourceGraph.GetAttributes(resourceType); + + return attributes; + } + + /// + /// By default, the client serializer does not include any relationships + /// for entities in the primary data unless explicitly included using + /// . + /// + private List GetRelationshipsToSerialize(IIdentifiable entity) + { + var currentResourceType = entity.GetType(); + /// only allow relationship attributes to be serialized if they were set using + /// + /// and the current is a main entry in the primary data. + if (!_relationshipsToSerializeCache.TryGetValue(currentResourceType, out var relationships)) + return new List(); + + return relationships; + } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/Client/ResponseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Client/ResponseDeserializer.cs new file mode 100644 index 0000000000..b5446325ad --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Client/ResponseDeserializer.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Serialization.Client +{ + /// + /// Client deserializer implementation of the + /// + public class ResponseDeserializer : BaseDocumentParser, IResponseDeserializer + { + public ResponseDeserializer(IResourceContextProvider provider) : base(provider) { } + + /// + public DeserializedSingleResponse DeserializeSingle(string body) where TResource : class, IIdentifiable + { + var entity = base.Deserialize(body); + return new DeserializedSingleResponse() + { + Links = _document.Links, + Meta = _document.Meta, + Data = entity == null ? null : (TResource)entity, + JsonApi = null, + Errors = null + }; + } + + /// + public DeserializedListResponse DeserializeList(string body) where TResource : class, IIdentifiable + { + var entities = base.Deserialize(body); + return new DeserializedListResponse() + { + Links = _document.Links, + Meta = _document.Meta, + Data = entities == null ? null : ((List)entities).Cast().ToList(), + JsonApi = null, + Errors = null + }; + } + + /// + /// Additional procesing required for client deserialization, responsible + /// for parsing the property. When a relationship value is parsed, + /// it goes through the included list to set its attributes and relationships. + /// + /// The entity that was constructed from the document's body + /// The metadata for the exposed field + /// Relationship data for . Is null when is not a + protected override void AfterProcessField(IIdentifiable entity, IResourceField field, RelationshipEntry data = null) + { + // Client deserializers do not need additional processing for attributes. + if (field is AttrAttribute) + return; + + // if the included property is empty or absent, there is no additional data to be parsed. + if (_document.Included == null || _document.Included.Count == 0) + return; + + if (field is HasOneAttribute hasOneAttr) + { // add attributes and relationships of a parsed HasOne relationship + var rio = data.SingleData; + if (rio == null) + hasOneAttr.SetValue(entity, null); + else + hasOneAttr.SetValue(entity, ParseIncludedRelationship(hasOneAttr, rio)); + } + else if (field is HasManyAttribute hasManyAttr) + { // add attributes and relationships of a parsed HasMany relationship + var values = TypeHelper.CreateListFor(hasManyAttr.RightType); + foreach (var rio in data.ManyData) + values.Add(ParseIncludedRelationship(hasManyAttr, rio)); + + hasManyAttr.SetValue(entity, values); + } + } + + /// + /// Searches for and parses the included relationship + /// + private IIdentifiable ParseIncludedRelationship(RelationshipAttribute relationshipAttr, ResourceIdentifierObject relatedResourceIdentifier) + { + var relatedInstance = relationshipAttr.RightType.New(); + relatedInstance.StringId = relatedResourceIdentifier.Id; + + var includedResource = GetLinkedResource(relatedResourceIdentifier); + if (includedResource == null) + return relatedInstance; + + var resourceContext = _provider.GetResourceContext(relatedResourceIdentifier.Type); + if (resourceContext == null) + throw new InvalidOperationException($"Included type '{relationshipAttr.RightType}' is not a registered json:api resource."); + + SetAttributes(relatedInstance, includedResource.Attributes, resourceContext.Attributes); + SetRelationships(relatedInstance, includedResource.Relationships, resourceContext.Relationships); + return relatedInstance; + } + + private ResourceObject GetLinkedResource(ResourceIdentifierObject relatedResourceIdentifier) + { + try + { + return _document.Included.SingleOrDefault(r => r.Type == relatedResourceIdentifier.Type && r.Id == relatedResourceIdentifier.Id); + } + catch (InvalidOperationException e) + { + throw new InvalidOperationException($"A compound document MUST NOT include more than one resource object for each type and id pair." + + $"The duplicate pair was '{relatedResourceIdentifier.Type}, {relatedResourceIdentifier.Id}'", e); + } + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs new file mode 100644 index 0000000000..ede8418eed --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace JsonApiDotNetCore.Serialization +{ + /// + /// Abstract base class for deserialization. Deserializes JSON content into s + /// And constructs instances of the resource(s) in the document body. + /// + public abstract class BaseDocumentParser + { + protected readonly IResourceContextProvider _provider; + protected Document _document; + + protected BaseDocumentParser(IResourceContextProvider provider) + { + _provider = provider; + } + + /// + /// This method is called each time an is constructed + /// from the serialized content, which is used to do additional processing + /// depending on the type of deserializers. + /// + /// + /// See the impementation of this method in + /// and for examples. + /// + /// The entity that was constructed from the document's body + /// The metadata for the exposed field + /// Relationship data for . Is null when is not a + protected abstract void AfterProcessField(IIdentifiable entity, IResourceField field, RelationshipEntry data = null); + + /// + protected object Deserialize(string body) + { + var bodyJToken = LoadJToken(body); + _document = bodyJToken.ToObject(); + if (_document.IsManyData) + { + if (_document.ManyData.Count == 0) + return new List(); + + return _document.ManyData.Select(ParseResourceObject).ToList(); + } + + if (_document.SingleData == null) return null; + return ParseResourceObject(_document.SingleData); + } + + /// + /// Sets the attributes on a parsed entity. + /// + /// The parsed entity + /// Attributes and their values, as in the serialized content + /// Exposed attributes for + /// + protected IIdentifiable SetAttributes(IIdentifiable entity, Dictionary attributeValues, List attributes) + { + if (attributeValues == null || attributeValues.Count == 0) + return entity; + + foreach (var attr in attributes) + { + if (attributeValues.TryGetValue(attr.PublicAttributeName, out object newValue)) + { + var convertedValue = ConvertAttrValue(newValue, attr.PropertyInfo.PropertyType); + attr.SetValue(entity, convertedValue); + AfterProcessField(entity, attr); + } + } + + return entity; + } + /// + /// Sets the relationships on a parsed entity + /// + /// The parsed entity + /// Relationships and their values, as in the serialized content + /// Exposed relatinships for + /// + protected IIdentifiable SetRelationships(IIdentifiable entity, Dictionary relationshipsValues, List relationshipAttributes) + { + if (relationshipsValues == null || relationshipsValues.Count == 0) + return entity; + + var entityProperties = entity.GetType().GetProperties(); + foreach (var attr in relationshipAttributes) + { + if (!relationshipsValues.TryGetValue(attr.PublicRelationshipName, out RelationshipEntry relationshipData) || !relationshipData.IsPopulated) + continue; + + if (attr is HasOneAttribute hasOne) + SetHasOneRelationship(entity, entityProperties, (HasOneAttribute)attr, relationshipData); + else + SetHasManyRelationship(entity, (HasManyAttribute)attr, relationshipData); + + } + return entity; + } + + private JToken LoadJToken(string body) + { + JToken jToken; + using (JsonReader jsonReader = new JsonTextReader(new StringReader(body))) + { + // https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/509 + jsonReader.DateParseHandling = DateParseHandling.None; + jToken = JToken.Load(jsonReader); + } + return jToken; + } + + /// + /// Creates an instance of the referenced type in + /// and sets its attributes and relationships + /// + /// + /// The parsed entity + private IIdentifiable ParseResourceObject(ResourceObject data) + { + var resourceContext = _provider.GetResourceContext(data.Type); + if (resourceContext == null) + { + throw new JsonApiException(400, + message: $"This API does not contain a json:api resource named '{data.Type}'.", + detail: "This resource is not registered on the ResourceGraph. " + + "If you are using Entity Framework, make sure the DbSet matches the expected resource name. " + + "If you have manually registered the resource, check that the call to AddResource correctly sets the public name."); + } + + var entity = (IIdentifiable)Activator.CreateInstance(resourceContext.ResourceType); + + entity = SetAttributes(entity, data.Attributes, resourceContext.Attributes); + entity = SetRelationships(entity, data.Relationships, resourceContext.Relationships); + + if (data.Id != null) + entity.StringId = data.Id?.ToString(); + + return entity; + } + + /// + /// Sets a HasOne relationship on a parsed entity. If present, also + /// populates the foreign key. + /// + /// + /// + /// + /// + /// + private object SetHasOneRelationship(IIdentifiable entity, + PropertyInfo[] entityProperties, + HasOneAttribute attr, + RelationshipEntry relationshipData) + { + var rio = (ResourceIdentifierObject)relationshipData.Data; + var relatedId = rio?.Id ?? null; + + // this does not make sense in the following case: if we're setting the dependent of a one-to-one relationship, IdentifiablePropertyName should be null. + var foreignKeyProperty = entityProperties.FirstOrDefault(p => p.Name == attr.IdentifiablePropertyName); + + if (foreignKeyProperty != null) + /// there is a FK from the current entity pointing to the related object, + /// i.e. we're populating the relationship from the dependent side. + SetForeignKey(entity, foreignKeyProperty, attr, relatedId); + + SetNavigation(entity, attr, relatedId); + + /// depending on if this base parser is used client-side or server-side, + /// different additional processing per field needs to be executed. + AfterProcessField(entity, attr, relationshipData); + + return entity; + } + + /// + /// Sets the dependent side of a HasOne relationship, which means that a + /// foreign key also will to be populated. + /// + private void SetForeignKey(IIdentifiable entity, PropertyInfo foreignKey, HasOneAttribute attr, string id) + { + bool foreignKeyPropertyIsNullableType = Nullable.GetUnderlyingType(foreignKey.PropertyType) != null + || foreignKey.PropertyType == typeof(string); + if (id == null && !foreignKeyPropertyIsNullableType) + { + // this happens when a non-optional relationship is deliberatedly set to null. + // For a server deserializer, it should be mapped to a BadRequest HTTP error code. + throw new FormatException($"Cannot set required relationship identifier '{attr.IdentifiablePropertyName}' to null because it is a non-nullable type."); + } + var convertedId = TypeHelper.ConvertType(id, foreignKey.PropertyType); + foreignKey.SetValue(entity, convertedId); + } + + /// + /// Sets the principal side of a HasOne relationship, which means no + /// foreign key is involved + /// + private void SetNavigation(IIdentifiable entity, HasOneAttribute attr, string relatedId) + { + if (relatedId == null) + { + attr.SetValue(entity, null); + } + else + { + var relatedInstance = attr.RightType.New(); + relatedInstance.StringId = relatedId; + attr.SetValue(entity, relatedInstance); + } + } + + /// + /// Sets a HasMany relationship. + /// + private object SetHasManyRelationship(IIdentifiable entity, + HasManyAttribute attr, + RelationshipEntry relationshipData) + { + if (relationshipData.Data != null) + { // if the relationship is set to null, no need to set the navigation property to null: this is the default value. + var relatedResources = relationshipData.ManyData.Select(rio => + { + var relatedInstance = attr.RightType.New(); + relatedInstance.StringId = rio.Id; + return relatedInstance; + }); + var convertedCollection = TypeHelper.ConvertCollection(relatedResources, attr.RightType); + attr.SetValue(entity, convertedCollection); + } + + AfterProcessField(entity, attr, relationshipData); + + return entity; + } + + private object ConvertAttrValue(object newValue, Type targetType) + { + if (newValue is JContainer jObject) + // the attribute value is a complex type that needs additional deserialization + return DeserializeComplexType(jObject, targetType); + + // the attribute value is a native C# type. + var convertedValue = TypeHelper.ConvertType(newValue, targetType); + return convertedValue; + } + + private object DeserializeComplexType(JContainer obj, Type targetType) + { + return obj.ToObject(targetType); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Common/DocumentBuilder.cs b/src/JsonApiDotNetCore/Serialization/Common/DocumentBuilder.cs new file mode 100644 index 0000000000..54e4066ecf --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Common/DocumentBuilder.cs @@ -0,0 +1,55 @@ +using System.Collections; +using System.Collections.Generic; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Serialization +{ + /// + /// Abstract base class for serialization. + /// Uses to convert entities in to s and wraps them in a . + /// + public abstract class BaseDocumentBuilder + { + protected readonly IResourceContextProvider _provider; + protected readonly IResourceObjectBuilder _resourceObjectBuilder; + protected BaseDocumentBuilder(IResourceObjectBuilder resourceObjectBuilder, IResourceContextProvider provider) + { + _resourceObjectBuilder = resourceObjectBuilder; + _provider = provider; + } + + /// + /// Builds a for . + /// Adds the attributes and relationships that are enlisted in and + /// + /// Entity to build a Resource Object for + /// Attributes to include in the building process + /// Relationships to include in the building process + /// The resource object that was built + protected Document Build(IIdentifiable entity, List attributes, List relationships) + { + if (entity == null) + return new Document(); + + return new Document { Data = _resourceObjectBuilder.Build(entity, attributes, relationships) }; + } + + /// + /// Builds a for . + /// Adds the attributes and relationships that are enlisted in and + /// + /// Entity to build a Resource Object for + /// Attributes to include in the building process + /// Relationships to include in the building process + /// The resource object that was built + protected Document Build(IEnumerable entities, List attributes, List relationships) + { + var data = new List(); + foreach (IIdentifiable entity in entities) + data.Add(_resourceObjectBuilder.Build(entity, attributes, relationships)); + + return new Document { Data = data }; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Common/IResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Common/IResourceObjectBuilder.cs new file mode 100644 index 0000000000..8c314d1ddc --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Common/IResourceObjectBuilder.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Serialization +{ + /// + /// Responsible for converting entities in to s + /// given a list of attributes and relationships. + /// + public interface IResourceObjectBuilder + { + /// + /// Converts into a . + /// Adds the attributes and relationships that are enlisted in and + /// + /// Entity to build a Resource Object for + /// Attributes to include in the building process + /// Relationships to include in the building process + /// The resource object that was built + ResourceObject Build(IIdentifiable entity, IEnumerable attributes, IEnumerable relationships); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilder.cs new file mode 100644 index 0000000000..6e0dde1fb0 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilder.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Serialization +{ + + /// + public class ResourceObjectBuilder : IResourceObjectBuilder + { + protected readonly IResourceContextProvider _provider; + private readonly ResourceObjectBuilderSettings _settings; + private const string _identifiablePropertyName = nameof(Identifiable.Id); + + public ResourceObjectBuilder(IResourceContextProvider provider, ResourceObjectBuilderSettings settings) + { + _provider = provider; + _settings = settings; + } + + /// + public ResourceObject Build(IIdentifiable entity, IEnumerable attributes = null, IEnumerable relationships = null) + { + var resourceContext = _provider.GetResourceContext(entity.GetType()); + + // populating the top-level "type" and "id" members. + var ro = new ResourceObject { Type = resourceContext.ResourceName, Id = entity.StringId.NullIfEmpty() }; + + // populating the top-level "attribute" member of a resource object. never include "id" as an attribute + if (attributes != null && (attributes = attributes.Where(attr => attr.InternalAttributeName != _identifiablePropertyName)).Any()) + ProcessAttributes(entity, attributes, ro); + + // populating the top-level "relationship" member of a resource object. + if (relationships != null) + ProcessRelationships(entity, relationships, ro); + + return ro; + } + + /// + /// Builds the entries of the "relationships + /// objects" The default behaviour is to just construct a resource linkage + /// with the "data" field populated with "single" or "many" data. + /// Depending on the requirements of the implementation (server or client serializer), + /// this may be overridden. + /// + protected virtual RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, IIdentifiable entity) + { + return new RelationshipEntry { Data = GetRelatedResourceLinkage(relationship, entity) }; + } + + /// + /// Gets the value for the property. + /// + protected object GetRelatedResourceLinkage(RelationshipAttribute relationship, IIdentifiable entity) + { + if (relationship is HasOneAttribute hasOne) + return GetRelatedResourceLinkage(hasOne, entity); + + return GetRelatedResourceLinkage((HasManyAttribute)relationship, entity); + } + + /// + /// Builds a for a HasOne relationship + /// + private ResourceIdentifierObject GetRelatedResourceLinkage(HasOneAttribute relationship, IIdentifiable entity) + { + var relatedEntity = (IIdentifiable)relationship.GetValue(entity); + if (relatedEntity == null && IsRequiredToOneRelationship(relationship, entity)) + throw new NotSupportedException("Cannot serialize a required to one relationship that is not populated but was included in the set of relationships to be serialized."); + + if (relatedEntity != null) + return GetResourceIdentifier(relatedEntity); + + return null; + } + + /// + /// Builds the s for a HasMany relationship + /// + private List GetRelatedResourceLinkage(HasManyAttribute relationship, IIdentifiable entity) + { + var relatedEntities = (IEnumerable)relationship.GetValue(entity); + var manyData = new List(); + if (relatedEntities != null) + foreach (IIdentifiable relatedEntity in relatedEntities) + manyData.Add(GetResourceIdentifier(relatedEntity)); + + return manyData; + } + + /// + /// Creates a from . + /// + private ResourceIdentifierObject GetResourceIdentifier(IIdentifiable entity) + { + var resourceName = _provider.GetResourceContext(entity.GetType()).ResourceName; + return new ResourceIdentifierObject + { + Type = resourceName, + Id = entity.StringId + }; + } + + /// + /// Checks if the to-one relationship is required by checking if the foreign key is nullable. + /// + private bool IsRequiredToOneRelationship(HasOneAttribute attr, IIdentifiable entity) + { + var foreignKey = entity.GetType().GetProperty(attr.IdentifiablePropertyName); + if (foreignKey != null && Nullable.GetUnderlyingType(foreignKey.PropertyType) == null) + return true; + + return false; + } + + /// + /// Puts the relationships of the entity into the resource object. + /// + private void ProcessRelationships(IIdentifiable entity, IEnumerable relationships, ResourceObject ro) + { + foreach (var rel in relationships) + { + var relData = GetRelationshipData(rel, entity); + if (relData != null) + (ro.Relationships = ro.Relationships ?? new Dictionary()).Add(rel.PublicRelationshipName, relData); + } + } + + /// + /// Puts the attributes of the entity into the resource object. + /// + private void ProcessAttributes(IIdentifiable entity, IEnumerable attributes, ResourceObject ro) + { + ro.Attributes = new Dictionary(); + foreach (var attr in attributes) + { + var value = attr.GetValue(entity); + if (!(value == default && _settings.OmitDefaultValuedAttributes) && !(value == null && _settings.OmitNullValuedAttributes)) + ro.Attributes.Add(attr.PublicAttributeName, value); + } + } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilderSettings.cs b/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilderSettings.cs new file mode 100644 index 0000000000..3b910e3e97 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilderSettings.cs @@ -0,0 +1,44 @@ +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Serialization +{ + /// + /// Options used to configure how fields of a model get serialized into + /// a json:api . + /// + public class ResourceObjectBuilderSettings + { + /// Omit null values from attributes + public ResourceObjectBuilderSettings(bool omitNullValuedAttributes = false, bool omitDefaultValuedAttributes = false) + { + OmitNullValuedAttributes = omitNullValuedAttributes; + OmitDefaultValuedAttributes = omitDefaultValuedAttributes; + } + + /// + /// Prevent attributes with null values from being included in the response. + /// This property is internal and if you want to enable this behavior, you + /// should do so on the . + /// + /// + /// + /// options.NullAttributeResponseBehavior = new NullAttributeResponseBehavior(true); + /// + /// + public bool OmitNullValuedAttributes { get; } + + /// + /// Prevent attributes with default values from being included in the response. + /// This property is internal and if you want to enable this behavior, you + /// should do so on the . + /// + /// + /// + /// options.DefaultAttributeResponseBehavior = new DefaultAttributeResponseBehavior(true); + /// + /// + public bool OmitDefaultValuedAttributes { get; } + } + +} + diff --git a/src/JsonApiDotNetCore/Serialization/DasherizedResolver.cs b/src/JsonApiDotNetCore/Serialization/DasherizedResolver.cs deleted file mode 100644 index 1b4a3aae6c..0000000000 --- a/src/JsonApiDotNetCore/Serialization/DasherizedResolver.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Reflection; -using JsonApiDotNetCore.Extensions; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace JsonApiDotNetCore.Serialization -{ - public class DasherizedResolver : DefaultContractResolver - { - protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) - { - JsonProperty property = base.CreateProperty(member, memberSerialization); - - property.PropertyName = property.PropertyName.Dasherize(); - - return property; - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiDeSerializer.cs deleted file mode 100644 index 6b6f41fbf7..0000000000 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiDeSerializer.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCore.Serialization -{ - public interface IJsonApiDeSerializer - { - object Deserialize(string requestBody); - TEntity Deserialize(string requestBody); - object DeserializeRelationship(string requestBody); - List DeserializeList(string requestBody); - object DocumentToObject(ResourceObject data, List included = null); - } -} diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs deleted file mode 100644 index 21eae09980..0000000000 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace JsonApiDotNetCore.Serialization -{ - public interface IJsonApiSerializer - { - string Serialize(object entity); - } -} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs deleted file mode 100644 index c11c011852..0000000000 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ /dev/null @@ -1,355 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Generics; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Operations; -using JsonApiDotNetCore.Services; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace JsonApiDotNetCore.Serialization -{ - public class JsonApiDeSerializer : IJsonApiDeSerializer - { - private readonly IJsonApiContext _jsonApiContext; - - public JsonApiDeSerializer(IJsonApiContext jsonApiContext) - { - _jsonApiContext = jsonApiContext; - } - - public object Deserialize(string requestBody) - { - try - { - JToken bodyJToken; - using (JsonReader jsonReader = new JsonTextReader(new StringReader(requestBody))) - { - jsonReader.DateParseHandling = DateParseHandling.None; - bodyJToken = JToken.Load(jsonReader); - } - if (RequestIsOperation(bodyJToken)) - { - _jsonApiContext.IsBulkOperationRequest = true; - - // TODO: determine whether or not the token should be re-used rather than performing full - // deserialization again from the string - var operations = JsonConvert.DeserializeObject(requestBody); - if (operations == null) - throw new JsonApiException(400, "Failed to deserialize operations request."); - - return operations; - } - - var document = bodyJToken.ToObject(); - - _jsonApiContext.DocumentMeta = document.Meta; - var entity = DocumentToObject(document.Data, document.Included); - return entity; - } - catch (JsonApiException) - { - throw; - } - catch (Exception e) - { - throw new JsonApiException(400, "Failed to deserialize request body", e); - } - } - - private bool RequestIsOperation(JToken bodyJToken) - => _jsonApiContext.Options.EnableOperations - && (bodyJToken.SelectToken("operations") != null); - - public TEntity Deserialize(string requestBody) => (TEntity)Deserialize(requestBody); - - public object DeserializeRelationship(string requestBody) - { - try - { - var data = JToken.Parse(requestBody)["data"]; - - if (data is JArray) - return data.ToObject>(); - - return new List { data.ToObject() }; - } - catch (Exception e) - { - throw new JsonApiException(400, "Failed to deserialize request body", e); - } - } - - public List DeserializeList(string requestBody) - { - try - { - var documents = JsonConvert.DeserializeObject(requestBody); - - var deserializedList = new List(); - foreach (var data in documents.Data) - { - var entity = (TEntity)DocumentToObject(data, documents.Included); - deserializedList.Add(entity); - } - - return deserializedList; - } - catch (Exception e) - { - throw new JsonApiException(400, "Failed to deserialize request body", e); - } - } - - public object DocumentToObject(ResourceObject data, List included = null) - { - if (data == null) - throw new JsonApiException(422, "Failed to deserialize document as json:api."); - - var contextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(data.Type?.ToString()); - _jsonApiContext.RequestEntity = contextEntity ?? throw new JsonApiException(400, - message: $"This API does not contain a json:api resource named '{data.Type}'.", - detail: "This resource is not registered on the ResourceGraph. " - + "If you are using Entity Framework, make sure the DbSet matches the expected resource name. " - + "If you have manually registered the resource, check that the call to AddResource correctly sets the public name."); - - var entity = Activator.CreateInstance(contextEntity.EntityType); - - entity = SetEntityAttributes(entity, contextEntity, data.Attributes); - entity = SetRelationships(entity, contextEntity, data.Relationships, included); - - var identifiableEntity = (IIdentifiable)entity; - - if (data.Id != null) - identifiableEntity.StringId = data.Id?.ToString(); - - return identifiableEntity; - } - - private object SetEntityAttributes( - object entity, ContextEntity contextEntity, Dictionary attributeValues) - { - if (attributeValues == null || attributeValues.Count == 0) - return entity; - - foreach (var attr in contextEntity.Attributes) - { - if (attributeValues.TryGetValue(attr.PublicAttributeName, out object newValue)) - { - if (attr.IsImmutable) - continue; - var convertedValue = ConvertAttrValue(newValue, attr.PropertyInfo.PropertyType); - attr.SetValue(entity, convertedValue); - /// todo: as a part of the process of decoupling JADNC (specifically - /// through the decoupling IJsonApiContext), we now no longer need to - /// store the updated relationship values in this property. For now - /// just assigning null as value, will remove this property later as a whole. - /// see #512 - _jsonApiContext.AttributesToUpdate[attr] = null; - } - } - - return entity; - } - - private object ConvertAttrValue(object newValue, Type targetType) - { - if (newValue is JContainer jObject) - return DeserializeComplexType(jObject, targetType); - - var convertedValue = TypeHelper.ConvertType(newValue, targetType); - return convertedValue; - } - - private object DeserializeComplexType(JContainer obj, Type targetType) - { - return obj.ToObject(targetType, JsonSerializer.Create(_jsonApiContext.Options.SerializerSettings)); - } - - private object SetRelationships( - object entity, - ContextEntity contextEntity, - Dictionary relationships, - List included = null) - { - if (relationships == null || relationships.Count == 0) - return entity; - - var entityProperties = entity.GetType().GetProperties(); - - foreach (var attr in contextEntity.Relationships) - { - entity = attr.IsHasOne - ? SetHasOneRelationship(entity, entityProperties, (HasOneAttribute)attr, contextEntity, relationships, included) - : SetHasManyRelationship(entity, entityProperties, (HasManyAttribute)attr, contextEntity, relationships, included); - } - - return entity; - } - - private object SetHasOneRelationship(object entity, - PropertyInfo[] entityProperties, - HasOneAttribute attr, - ContextEntity contextEntity, - Dictionary relationships, - List included = null) - { - var relationshipName = attr.PublicRelationshipName; - - if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData) == false) - return entity; - - var rio = (ResourceIdentifierObject)relationshipData.ExposedData; - - var foreignKey = attr.IdentifiablePropertyName; - var foreignKeyProperty = entityProperties.FirstOrDefault(p => p.Name == foreignKey); - if (foreignKeyProperty == null && rio == null) - return entity; - - SetHasOneForeignKeyValue(entity, attr, foreignKeyProperty, rio); - SetHasOneNavigationPropertyValue(entity, attr, rio, included); - - // recursive call ... - if (included != null) - { - var navigationPropertyValue = attr.GetValue(entity); - - var resourceGraphEntity = _jsonApiContext.ResourceGraph.GetContextEntity(attr.DependentType); - if(navigationPropertyValue != null && resourceGraphEntity != null) - - { - var includedResource = included.SingleOrDefault(r => r.Type == rio.Type && r.Id == rio.Id); - if (includedResource != null) - SetRelationships(navigationPropertyValue, resourceGraphEntity, includedResource.Relationships, included); - } - } - - return entity; - } - - private void SetHasOneForeignKeyValue(object entity, HasOneAttribute hasOneAttr, PropertyInfo foreignKeyProperty, ResourceIdentifierObject rio) - { - var foreignKeyPropertyValue = rio?.Id ?? null; - if (foreignKeyProperty != null) - { - // in the case of the HasOne independent side of the relationship, we should still create the shell entity on the other side - // we should not actually require the resource to have a foreign key (be the dependent side of the relationship) - - // e.g. PATCH /articles - // {... { "relationships":{ "Owner": { "data": null } } } } - bool foreignKeyPropertyIsNullableType = Nullable.GetUnderlyingType(foreignKeyProperty.PropertyType) != null - || foreignKeyProperty.PropertyType == typeof(string); - if (rio == null && !foreignKeyPropertyIsNullableType) - throw new JsonApiException(400, $"Cannot set required relationship identifier '{hasOneAttr.IdentifiablePropertyName}' to null because it is a non-nullable type."); - - var convertedValue = TypeHelper.ConvertType(foreignKeyPropertyValue, foreignKeyProperty.PropertyType); - /// todo: as a part of the process of decoupling JADNC (specifically - /// through the decoupling IJsonApiContext), we now no longer need to - /// store the updated relationship values in this property. For now - /// just assigning null as value, will remove this property later as a whole. - /// see #512 - if (convertedValue == null) _jsonApiContext.HasOneRelationshipPointers.Add(hasOneAttr, null); - } - } - - /// - /// Sets the value of the navigation property for the related resource. - /// If the resource has been included, all attributes will be set. - /// If the resource has not been included, only the id will be set. - /// - private void SetHasOneNavigationPropertyValue(object entity, HasOneAttribute hasOneAttr, ResourceIdentifierObject rio, List included) - { - // if the resource identifier is null, there should be no reason to instantiate an instance - if (rio != null && rio.Id != null) - { - // we have now set the FK property on the resource, now we need to check to see if the - // related entity was included in the payload and update its attributes - var includedRelationshipObject = GetIncludedRelationship(rio, included, hasOneAttr); - if (includedRelationshipObject != null) - hasOneAttr.SetValue(entity, includedRelationshipObject); - - /// todo: as a part of the process of decoupling JADNC (specifically - /// through the decoupling IJsonApiContext), we now no longer need to - /// store the updated relationship values in this property. For now - /// just assigning null as value, will remove this property later as a whole. - /// see #512 - _jsonApiContext.HasOneRelationshipPointers.Add(hasOneAttr, null); - } - } - - private object SetHasManyRelationship(object entity, - PropertyInfo[] entityProperties, - HasManyAttribute attr, - ContextEntity contextEntity, - Dictionary relationships, - List included = null) - { - var relationshipName = attr.PublicRelationshipName; - - if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData)) - { - if (relationshipData.IsHasMany == false || relationshipData.ManyData == null) - return entity; - - var relatedResources = relationshipData.ManyData.Select(r => - { - var instance = GetIncludedRelationship(r, included, attr); - return instance; - }); - - var convertedCollection = TypeHelper.ConvertCollection(relatedResources, attr.DependentType); - - attr.SetValue(entity, convertedCollection); - /// todo: as a part of the process of decoupling JADNC (specifically - /// through the decoupling IJsonApiContext), we now no longer need to - /// store the updated relationship values in this property. For now - /// just assigning null as value, will remove this property later as a whole. - /// see #512 - _jsonApiContext.HasManyRelationshipPointers.Add(attr, null); - } - - return entity; - } - - private IIdentifiable GetIncludedRelationship(ResourceIdentifierObject relatedResourceIdentifier, List includedResources, RelationshipAttribute relationshipAttr) - { - // at this point we can be sure the relationshipAttr.Type is IIdentifiable because we were able to successfully build the ResourceGraph - var relatedInstance = relationshipAttr.DependentType.New(); - relatedInstance.StringId = relatedResourceIdentifier.Id; - - // can't provide any more data other than the rio since it is not contained in the included section - if (includedResources == null || includedResources.Count == 0) - return relatedInstance; - - var includedResource = GetLinkedResource(relatedResourceIdentifier, includedResources); - if (includedResource == null) - return relatedInstance; - - var contextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationshipAttr.DependentType); - if (contextEntity == null) - throw new JsonApiException(400, $"Included type '{relationshipAttr.DependentType}' is not a registered json:api resource."); - - SetEntityAttributes(relatedInstance, contextEntity, includedResource.Attributes); - - return relatedInstance; - } - - private ResourceObject GetLinkedResource(ResourceIdentifierObject relatedResourceIdentifier, List includedResources) - { - try - { - return includedResources.SingleOrDefault(r => r.Type == relatedResourceIdentifier.Type && r.Id == relatedResourceIdentifier.Id); - } - catch (InvalidOperationException e) - { - throw new JsonApiException(400, $"A compound document MUST NOT include more than one resource object for each type and id pair." - + $"The duplicate pair was '{relatedResourceIdentifier.Type}, {relatedResourceIdentifier.Id}'", e); - } - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs deleted file mode 100644 index a784554f58..0000000000 --- a/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Serialization -{ - public class JsonApiSerializer : IJsonApiSerializer - { - private readonly IDocumentBuilder _documentBuilder; - private readonly ILogger _logger; - private readonly IJsonApiContext _jsonApiContext; - - public JsonApiSerializer( - IJsonApiContext jsonApiContext, - IDocumentBuilder documentBuilder) - { - _jsonApiContext = jsonApiContext; - _documentBuilder = documentBuilder; - } - - public JsonApiSerializer( - IJsonApiContext jsonApiContext, - IDocumentBuilder documentBuilder, - ILoggerFactory loggerFactory) - { - _jsonApiContext = jsonApiContext; - _documentBuilder = documentBuilder; - _logger = loggerFactory?.CreateLogger(); - } - - public string Serialize(object entity) - { - if (entity == null) - return GetNullDataResponse(); - - if (entity.GetType() == typeof(ErrorCollection) || (_jsonApiContext.RequestEntity == null && _jsonApiContext.IsBulkOperationRequest == false)) - return GetErrorJson(entity, _logger); - - if (_jsonApiContext.IsBulkOperationRequest) - return _serialize(entity); - - if (entity is IEnumerable) - return SerializeDocuments(entity); - - return SerializeDocument(entity); - } - - private string GetNullDataResponse() - { - return JsonConvert.SerializeObject(new Document - { - Data = null - }); - } - - private string GetErrorJson(object responseObject, ILogger logger) - { - if (responseObject is ErrorCollection errorCollection) - { - return errorCollection.GetJson(); - } - else - { - if (logger?.IsEnabled(LogLevel.Information) == true) - { - logger.LogInformation("Response was not a JSONAPI entity. Serializing as plain JSON."); - } - - return JsonConvert.SerializeObject(responseObject); - } - } - - private string SerializeDocuments(object entity) - { - var entities = entity as IEnumerable; - var documents = _documentBuilder.Build(entities); - return _serialize(documents); - } - - private string SerializeDocument(object entity) - { - var identifiableEntity = entity as IIdentifiable; - var document = _documentBuilder.Build(identifiableEntity); - return _serialize(document); - } - - private string _serialize(object obj) - { - return JsonConvert.SerializeObject(obj, _jsonApiContext.Options.SerializerSettings); - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Server/Builders/IncludedResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Server/Builders/IncludedResourceObjectBuilder.cs new file mode 100644 index 0000000000..4ce2aa5f94 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Server/Builders/IncludedResourceObjectBuilder.cs @@ -0,0 +1,134 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Serialization.Server.Builders +{ + /// + public class IncludedResourceObjectBuilder : ResourceObjectBuilder, IIncludedResourceObjectBuilder + { + private readonly HashSet _included; + private readonly IFieldsToSerialize _fieldsToSerialize; + private readonly ILinkBuilder _linkBuilder; + + public IncludedResourceObjectBuilder(IFieldsToSerialize fieldsToSerialize, + ILinkBuilder linkBuilder, + IResourceContextProvider provider, + IResourceObjectBuilderSettingsProvider settingsProvider) + : base(provider, settingsProvider.Get()) + { + _included = new HashSet(new ResourceObjectComparer()); + _fieldsToSerialize = fieldsToSerialize; + _linkBuilder = linkBuilder; + } + + /// + public List Build() + { + if (_included.Any()) + { + // cleans relationship dictionaries and adds links of resources. + foreach (var resourceObject in _included) + { + if (resourceObject.Relationships != null) + { /// removes relationship entries (s) if they're completely empty. + var pruned = resourceObject.Relationships.Where(p => p.Value.IsPopulated || p.Value.Links != null).ToDictionary(p => p.Key, p => p.Value); + if (!pruned.Any()) pruned = null; + resourceObject.Relationships = pruned; + } + resourceObject.Links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); + } + return _included.ToList(); + } + return null; + } + + /// + public void IncludeRelationshipChain(List inclusionChain, IIdentifiable rootEntity) + { + /// We dont have to build a resource object for the root entity because + /// this one is already encoded in the documents primary data, so we process the chain + /// starting from the first related entity. + var relationship = inclusionChain.First(); + var chainRemainder = ShiftChain(inclusionChain); + var related = relationship.GetValue(rootEntity); + ProcessChain(relationship, related, chainRemainder); + } + + private void ProcessChain(RelationshipAttribute originRelationship, object related, List inclusionChain) + { + if (related is IEnumerable children) + foreach (IIdentifiable child in children) + ProcessRelationship(originRelationship, child, inclusionChain); + else + ProcessRelationship(originRelationship, (IIdentifiable)related, inclusionChain); + } + + private void ProcessRelationship(RelationshipAttribute originRelationship, IIdentifiable parent, List inclusionChain) + { + // get the resource object for parent. + var resourceObject = GetOrBuildResourceObject(parent, originRelationship); + if (!inclusionChain.Any()) + return; + var nextRelationship = inclusionChain.First(); + var chainRemainder = inclusionChain.ToList(); + chainRemainder.RemoveAt(0); + + var nextRelationshipName = nextRelationship.PublicRelationshipName; + var relationshipsObject = resourceObject.Relationships; + // add the relationship entry in the relationship object. + if (!relationshipsObject.TryGetValue(nextRelationshipName, out var relationshipEntry)) + relationshipsObject[nextRelationshipName] = (relationshipEntry = GetRelationshipData(nextRelationship, parent)); + + relationshipEntry.Data = GetRelatedResourceLinkage(nextRelationship, parent); + + if (relationshipEntry.HasResource) + { // if the relationship is set, continue parsing the chain. + var related = nextRelationship.GetValue(parent); + ProcessChain(nextRelationship, related, chainRemainder); + } + } + + private List ShiftChain(List chain) + { + var chainRemainder = chain.ToList(); + chainRemainder.RemoveAt(0); + return chainRemainder; + } + + /// + /// We only need a empty relationship object entry here. It will be populated in the + /// ProcessRelationships method. + /// + /// + /// + /// + protected override RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, IIdentifiable entity) + { + return new RelationshipEntry { Links = _linkBuilder.GetRelationshipLinks(relationship, entity) }; + } + + /// + /// Gets the resource object for by searching the included list. + /// If it was not already build, it is constructed and added to the included list. + /// + /// + /// + /// + private ResourceObject GetOrBuildResourceObject(IIdentifiable parent, RelationshipAttribute relationship) + { + var type = parent.GetType(); + var resourceName = _provider.GetResourceContext(type).ResourceName; + var entry = _included.SingleOrDefault(ro => ro.Type == resourceName && ro.Id == parent.StringId); + if (entry == null) + { + entry = Build(parent, _fieldsToSerialize.GetAllowedAttributes(type, relationship), _fieldsToSerialize.GetAllowedRelationships(type)); + _included.Add(entry); + } + return entry; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs new file mode 100644 index 0000000000..9b00cdc23a --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs @@ -0,0 +1,167 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.Links; +using JsonApiDotNetCore.Query; + +namespace JsonApiDotNetCore.Serialization.Server.Builders +{ + public class LinkBuilder : ILinkBuilder + { + private readonly IResourceContextProvider _provider; + private readonly ILinksConfiguration _options; + private readonly ICurrentRequest _currentRequest; + private readonly IPageService _pageService; + + public LinkBuilder(ILinksConfiguration options, + ICurrentRequest currentRequest, + IPageService pageService, + IResourceContextProvider provider) + { + _options = options; + _currentRequest = currentRequest; + _pageService = pageService; + _provider = provider; + } + + /// + public TopLevelLinks GetTopLevelLinks(ResourceContext primaryResource) + { + TopLevelLinks topLevelLinks = null; + if (ShouldAddTopLevelLink(primaryResource, Link.Self)) + topLevelLinks = new TopLevelLinks { Self = GetSelfTopLevelLink(primaryResource.ResourceName) }; + + if (ShouldAddTopLevelLink(primaryResource, Link.Paging)) + SetPageLinks(primaryResource, ref topLevelLinks); + + return topLevelLinks; + } + + /// + /// Checks if the top-level should be added by first checking + /// configuration on the , and if not configured, by checking with the + /// global configuration in . + /// + /// + private bool ShouldAddTopLevelLink(ResourceContext primaryResource, Link link) + { + if (primaryResource.TopLevelLinks != Link.NotConfigured) + return primaryResource.TopLevelLinks.HasFlag(link); + return _options.TopLevelLinks.HasFlag(link); + } + + private void SetPageLinks(ResourceContext primaryResource, ref TopLevelLinks links) + { + if (!_pageService.ShouldPaginate()) + return; + + links = links ?? new TopLevelLinks(); + + if (_pageService.CurrentPage > 1) + { + links.First = GetPageLink(primaryResource, 1, _pageService.PageSize); + links.Prev = GetPageLink(primaryResource, _pageService.CurrentPage - 1, _pageService.PageSize); + } + + if (_pageService.CurrentPage < _pageService.TotalPages) + links.Next = GetPageLink(primaryResource, _pageService.CurrentPage + 1, _pageService.PageSize); + + + if (_pageService.TotalPages > 0) + links.Last = GetPageLink(primaryResource, _pageService.TotalPages, _pageService.PageSize); + } + + private string GetSelfTopLevelLink(string resourceName) + { + return $"{GetBasePath()}/{resourceName}"; + } + + private string GetPageLink(ResourceContext primaryResource, int pageOffset, int pageSize) + { + return $"{GetBasePath()}/{primaryResource.ResourceName}?page[size]={pageSize}&page[number]={pageOffset}"; + } + + + /// + public ResourceLinks GetResourceLinks(string resourceName, string id) + { + var resourceContext = _provider.GetResourceContext(resourceName); + if (ShouldAddResourceLink(resourceContext, Link.Self)) + return new ResourceLinks { Self = GetSelfResourceLink(resourceName, id) }; + + return null; + } + + /// + public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable parent) + { + var parentResourceContext = _provider.GetResourceContext(parent.GetType()); + var childNavigation = relationship.PublicRelationshipName; + RelationshipLinks links = null; + if (ShouldAddRelationshipLink(parentResourceContext, relationship, Link.Related)) + links = new RelationshipLinks { Related = GetRelatedRelationshipLink(parentResourceContext.ResourceName, parent.StringId, childNavigation) }; + + if (ShouldAddRelationshipLink(parentResourceContext, relationship, Link.Self)) + { + links = links ?? new RelationshipLinks(); + links.Self = GetSelfRelationshipLink(parentResourceContext.ResourceName, parent.StringId, childNavigation); + } + + return links; + } + + + private string GetSelfRelationshipLink(string parent, string parentId, string navigation) + { + return $"{GetBasePath()}/{parent}/{parentId}/relationships/{navigation}"; + } + + private string GetSelfResourceLink(string resource, string resourceId) + { + return $"{GetBasePath()}/{resource}/{resourceId}"; + } + + private string GetRelatedRelationshipLink(string parent, string parentId, string navigation) + { + return $"{GetBasePath()}/{parent}/{parentId}/{navigation}"; + } + + /// + /// Checks if the resource object level should be added by first checking + /// configuration on the , and if not configured, by checking with the + /// global configuration in . + /// + /// + private bool ShouldAddResourceLink(ResourceContext resourceContext, Link link) + { + if (resourceContext.ResourceLinks != Link.NotConfigured) + return resourceContext.ResourceLinks.HasFlag(link); + return _options.ResourceLinks.HasFlag(link); + } + + /// + /// Checks if the resource object level should be added by first checking + /// configuration on the attribute, if not configured by checking + /// the , and if not configured by checking with the + /// global configuration in . + /// + /// + private bool ShouldAddRelationshipLink(ResourceContext resourceContext, RelationshipAttribute relationship, Link link) + { + if (relationship.RelationshipLinks != Link.NotConfigured) + return relationship.RelationshipLinks.HasFlag(link); + if (resourceContext.RelationshipLinks != Link.NotConfigured) + return resourceContext.RelationshipLinks.HasFlag(link); + return _options.RelationshipLinks.HasFlag(link); + } + + protected string GetBasePath() + { + if (_options.RelativeLinks) + return string.Empty; + return _currentRequest.BasePath; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Server/Builders/MetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Server/Builders/MetaBuilder.cs new file mode 100644 index 0000000000..1a495909a3 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Server/Builders/MetaBuilder.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Query; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Serialization.Server.Builders +{ + /// + public class MetaBuilder : IMetaBuilder where T : class, IIdentifiable + { + private Dictionary _meta = new Dictionary(); + private readonly IPageService _pageService; + private readonly IJsonApiOptions _options; + private readonly IRequestMeta _requestMeta; + private readonly IHasMeta _resourceMeta; + + public MetaBuilder(IPageService pageService, + IJsonApiOptions options, + IRequestMeta requestMeta = null, + ResourceDefinition resourceDefinition = null) + { + _pageService = pageService; + _options = options; + _requestMeta = requestMeta; + _resourceMeta = resourceDefinition as IHasMeta; + } + /// + public void Add(string key, object value) + { + _meta[key] = value; + } + + /// + public void Add(Dictionary values) + { + _meta = values.Keys.Union(_meta.Keys) + .ToDictionary(key => key, + key => values.ContainsKey(key) ? values[key] : _meta[key]); + } + + /// + public Dictionary GetMeta() + { + if (_options.IncludeTotalRecordCount && _pageService.TotalRecords != null) + _meta.Add("total-records", _pageService.TotalRecords); + + if (_requestMeta != null) + Add(_requestMeta.GetMeta()); + + if (_resourceMeta != null) + Add(_resourceMeta.GetMeta()); + + if (_meta.Any()) return _meta; + return null; + } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/Server/Builders/ResponseResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Server/Builders/ResponseResourceObjectBuilder.cs new file mode 100644 index 0000000000..fadc819581 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Server/Builders/ResponseResourceObjectBuilder.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Query; +using JsonApiDotNetCore.Serialization.Server.Builders; + +namespace JsonApiDotNetCore.Serialization.Server +{ + public class ResponseResourceObjectBuilder : ResourceObjectBuilder, IResourceObjectBuilder + { + private readonly IIncludedResourceObjectBuilder _includedBuilder; + private readonly IIncludeService _includeService; + private readonly ILinkBuilder _linkBuilder; + private RelationshipAttribute _requestRelationship; + + public ResponseResourceObjectBuilder(ILinkBuilder linkBuilder, + IIncludedResourceObjectBuilder includedBuilder, + IIncludeService includeService, + IResourceContextProvider provider, + IResourceObjectBuilderSettingsProvider settingsProvider) + : base(provider, settingsProvider.Get()) + { + _linkBuilder = linkBuilder; + _includedBuilder = includedBuilder; + _includeService = includeService; + } + + public RelationshipEntry Build(IIdentifiable entity, RelationshipAttribute requestRelationship) + { + _requestRelationship = requestRelationship; + return GetRelationshipData(requestRelationship, entity); + } + + /// + /// Builds the values of the relationships object on a resource object. + /// The server serializer only populates the "data" member when the relationship is included, + /// and adds links unless these are turned off. This means that if a relationship is not included + /// and links are turned off, the entry would be completely empty, ie { }, which is not conform + /// json:api spec. In that case we return null which will omit the entry from the output. + /// + protected override RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, IIdentifiable entity) + { + RelationshipEntry relationshipEntry = null; + List> relationshipChains = null; + if (relationship == _requestRelationship || ShouldInclude(relationship, out relationshipChains )) + { + relationshipEntry = base.GetRelationshipData(relationship, entity); + if (relationshipChains != null && relationshipEntry.HasResource) + foreach (var chain in relationshipChains) + // traverses (recursively) and extracts all (nested) related entities for the current inclusion chain. + _includedBuilder.IncludeRelationshipChain(chain, entity); + } + + var links = _linkBuilder.GetRelationshipLinks(relationship, entity); + if (links != null) + // if links relationshiplinks should be built for this entry, populate the "links" field. + (relationshipEntry = relationshipEntry ?? new RelationshipEntry()).Links = links; + + /// if neither "links" nor "data" was popupated, return null, which will omit this entry from the output. + /// (see the NullValueHandling settings on ) + return relationshipEntry; + } + + /// + /// Inspects the included relationship chains (see + /// to see if should be included or not. + /// + private bool ShouldInclude(RelationshipAttribute relationship, out List> inclusionChain) + { + inclusionChain = _includeService.Get()?.Where(l => l.First().Equals(relationship)).ToList(); + if (inclusionChain == null || !inclusionChain.Any()) + return false; + return true; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IFieldsToSerialize.cs b/src/JsonApiDotNetCore/Serialization/Server/Contracts/IFieldsToSerialize.cs new file mode 100644 index 0000000000..2dfd261ebd --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Server/Contracts/IFieldsToSerialize.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Serialization.Server +{ + /// + /// Responsible for getting the set of fields that are to be included for a + /// given type in the serialization result. Typically combines various sources + /// of information, like application-wide hidden fields as set in + /// , or request-wide hidden fields + /// through sparse field selection. + /// + public interface IFieldsToSerialize + { + /// + /// Gets the list of attributes that are allowed to be serialized for + /// resource of type + /// if , it will consider the allowed list of attributes + /// as an included relationship + /// + List GetAllowedAttributes(Type type, RelationshipAttribute relationship = null); + /// + /// Gets the list of relationships that are allowed to be serialized for + /// resource of type + /// + List GetAllowedRelationships(Type type); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IIncludedResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Server/Contracts/IIncludedResourceObjectBuilder.cs new file mode 100644 index 0000000000..9bd77ec1d6 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Server/Contracts/IIncludedResourceObjectBuilder.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Serialization.Server.Builders +{ + public interface IIncludedResourceObjectBuilder + { + /// + /// Gets the list of resource objects representing the included entities + /// + List Build(); + /// + /// Extracts the included entities from using the + /// (arbitrarly deeply nested) included relationships in . + /// + void IncludeRelationshipChain(List inclusionChain, IIdentifiable rootEntity); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiDeserializer.cs new file mode 100644 index 0000000000..3767aae6ca --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiDeserializer.cs @@ -0,0 +1,17 @@ +using JsonApiDotNetCore.Models; +namespace JsonApiDotNetCore.Serialization.Server +{ + /// + /// Deserializer used internally in JsonApiDotNetCore to deserialize requests. + /// + public interface IJsonApiDeserializer + { + /// + /// Deserializes JSON in to a and constructs entities + /// from + /// + /// The JSON to be deserialized + /// The entities constructed from the content + object Deserialize(string body); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiSerializer.cs b/src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiSerializer.cs new file mode 100644 index 0000000000..46281e1e85 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiSerializer.cs @@ -0,0 +1,15 @@ +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Serialization.Server +{ + /// + /// Serializer used internally in JsonApiDotNetCore to serialize responses. + /// + public interface IJsonApiSerializer + { + /// + /// Serializes a single entity or a list of entities. + /// + string Serialize(object content); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiSerializerFactory.cs b/src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiSerializerFactory.cs new file mode 100644 index 0000000000..ab9502e666 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiSerializerFactory.cs @@ -0,0 +1,12 @@ +using System; + +namespace JsonApiDotNetCore.Serialization.Server +{ + public interface IJsonApiSerializerFactory + { + /// + /// Instantiates the serializer to process the servers response. + /// + IJsonApiSerializer GetSerializer(); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/Server/Contracts/ILinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Server/Contracts/ILinkBuilder.cs new file mode 100644 index 0000000000..a4bd87195a --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Server/Contracts/ILinkBuilder.cs @@ -0,0 +1,29 @@ +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.Links; + +namespace JsonApiDotNetCore.Serialization.Server.Builders +{ + /// + /// Builds resource object links and relationship object links. + /// + public interface ILinkBuilder + { + /// + /// Builds the links object that is included in the top-level of the document. + /// + /// The primary resource of the response body + TopLevelLinks GetTopLevelLinks(ResourceContext primaryResource); + /// + /// Builds the links object for resources in the primary data. + /// + /// + ResourceLinks GetResourceLinks(string resourceName, string id); + /// + /// Builds the links object that is included in the values of the . + /// + /// + /// + RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable parent); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IMetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Server/Contracts/IMetaBuilder.cs new file mode 100644 index 0000000000..5e18f930a5 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Server/Contracts/IMetaBuilder.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Serialization.Server.Builders +{ + /// + /// Builds the top-level meta data object. This builder is generic to allow for + /// different top-level meta data object depending on the associated resource of the request. + /// + /// Associated resource for which to build the meta data + public interface IMetaBuilder where TResource : class, IIdentifiable + { + /// + /// Adds a key-value pair to the top-level meta data object + /// + void Add(string key, object value); + /// + /// Joins the new dictionary with the current one. In the event of a key collision, + /// the new value will override the old. + /// + void Add(Dictionary values); + /// + /// Builds the top-level meta data object. + /// + Dictionary GetMeta(); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IResourceObjectBuilderSettingsProvider.cs b/src/JsonApiDotNetCore/Serialization/Server/Contracts/IResourceObjectBuilderSettingsProvider.cs new file mode 100644 index 0000000000..5da12fe37a --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Server/Contracts/IResourceObjectBuilderSettingsProvider.cs @@ -0,0 +1,13 @@ +namespace JsonApiDotNetCore.Serialization.Server +{ + /// + /// Service that provides the server serializer with + /// + public interface IResourceObjectBuilderSettingsProvider + { + /// + /// Gets the behaviour for the serializer it is injected in. + /// + ResourceObjectBuilderSettings Get(); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/Server/Contracts/IResponseSerializer.cs new file mode 100644 index 0000000000..f69e1ce096 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Server/Contracts/IResponseSerializer.cs @@ -0,0 +1,13 @@ +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Serialization.Server +{ + internal interface IResponseSerializer + { + /// + /// Sets the designated request relationship in the case of requests of + /// the form a /articles/1/relationships/author. + /// + RelationshipAttribute RequestRelationship { get; set; } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/Server/FieldsToSerialize.cs b/src/JsonApiDotNetCore/Serialization/Server/FieldsToSerialize.cs new file mode 100644 index 0000000000..4bdf99654a --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Server/FieldsToSerialize.cs @@ -0,0 +1,62 @@ +using JsonApiDotNetCore.Internal.Contracts; +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Query; +using System.Linq; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Serialization.Server +{ + /// + public class FieldsToSerialize : IFieldsToSerialize + { + private readonly IResourceGraph _resourceGraph; + private readonly ISparseFieldsService _sparseFieldsService ; + private readonly IResourceDefinitionProvider _provider; + + public FieldsToSerialize(IResourceGraph resourceGraph, + ISparseFieldsService sparseFieldsService, + IResourceDefinitionProvider provider) + { + _resourceGraph = resourceGraph; + _sparseFieldsService = sparseFieldsService; + _provider = provider; + } + + /// + public List GetAllowedAttributes(Type type, RelationshipAttribute relationship = null) + { // get the list of all exposed atttributes for the given type. + var allowed = _resourceGraph.GetAttributes(type); + + var resourceDefinition = _provider.Get(type); + if (resourceDefinition != null) + // The set of allowed attribrutes to be exposed was defined on the resource definition + allowed = allowed.Intersect(resourceDefinition.GetAllowedAttributes()).ToList(); + + var sparseFieldsSelection = _sparseFieldsService.Get(relationship); + if (sparseFieldsSelection != null && sparseFieldsSelection.Any()) + // from the allowed attributes, select the ones flagged by sparse field selection. + allowed = allowed.Intersect(sparseFieldsSelection).ToList(); + + return allowed; + } + + /// + /// + /// Note: this method does NOT check if a relationship is included to determine + /// if it should be serialized. This is because completely hiding a relationship + /// is not the same as not including. In the case of the latter, + /// we may still want to add the relationship to expose the navigation link to the client. + /// + public List GetAllowedRelationships(Type type) + { + var resourceDefinition = _provider.Get(type); + if (resourceDefinition != null) + // The set of allowed attribrutes to be exposed was defined on the resource definition + return resourceDefinition.GetAllowedRelationships(); + + // The set of allowed attribrutes to be exposed was NOT defined on the resource definition: return all + return _resourceGraph.GetRelationships(type); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs new file mode 100644 index 0000000000..441f873bd1 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs @@ -0,0 +1,46 @@ +using System; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Serialization.Server +{ + /// + /// Server deserializer implementation of the + /// + public class RequestDeserializer : BaseDocumentParser, IJsonApiDeserializer + { + private readonly ITargetedFields _targetedFields; + + public RequestDeserializer(IResourceContextProvider provider, + ITargetedFields targetedFields) : base(provider) + { + _targetedFields = targetedFields; + } + + /// + public new object Deserialize(string body) + { + return base.Deserialize(body); + } + + /// + /// Additional procesing required for server deserialization. Flags a + /// processed attribute or relationship as updated using . + /// + /// The entity that was constructed from the document's body + /// The metadata for the exposed field + /// Relationship data for . Is null when is not a + protected override void AfterProcessField(IIdentifiable entity, IResourceField field, RelationshipEntry data = null) + { + if (field is AttrAttribute attr) + { + if (!attr.IsImmutable) + _targetedFields.Attributes.Add(attr); + else + throw new InvalidOperationException($"Attribute {attr.PublicAttributeName} is immutable and therefore cannot be updated."); + } + else if (field is RelationshipAttribute relationship) + _targetedFields.Relationships.Add(relationship); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs b/src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs new file mode 100644 index 0000000000..9e4541c201 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs @@ -0,0 +1,28 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Query; + +namespace JsonApiDotNetCore.Serialization.Server +{ + /// + /// This implementation of the behaviour provider reads the query params that + /// can, if provided, override the settings in . + /// + public class ResourceObjectBuilderSettingsProvider : IResourceObjectBuilderSettingsProvider + { + private readonly IOmitDefaultService _defaultAttributeValues; + private readonly IOmitNullService _nullAttributeValues; + + public ResourceObjectBuilderSettingsProvider(IOmitDefaultService defaultAttributeValues, + IOmitNullService nullAttributeValues) + { + _defaultAttributeValues = defaultAttributeValues; + _nullAttributeValues = nullAttributeValues; + } + + /// + public ResourceObjectBuilderSettings Get() + { + return new ResourceObjectBuilderSettings(_nullAttributeValues.Config, _defaultAttributeValues.Config); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs new file mode 100644 index 0000000000..e54a0abd0a --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Query; +using Newtonsoft.Json; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Serialization.Server.Builders; +using JsonApiDotNetCore.Internal; + +namespace JsonApiDotNetCore.Serialization.Server +{ + + /// + /// Server serializer implementation of + /// + /// + /// Because in JsonApiDotNetCore every json:api request is associated with exactly one + /// resource (the request resource, see ), + /// the serializer can leverage this information using generics. + /// See for how this is instantiated. + /// + /// Type of the resource associated with the scope of the request + /// for which this serializer is used. + public class ResponseSerializer : BaseDocumentBuilder, IJsonApiSerializer, IResponseSerializer + where TResource : class, IIdentifiable + { + public RelationshipAttribute RequestRelationship { get; set; } + private readonly Dictionary> _attributesToSerializeCache = new Dictionary>(); + private readonly Dictionary> _relationshipsToSerializeCache = new Dictionary>(); + private readonly IIncludeService _includeService; + private readonly IFieldsToSerialize _fieldsToSerialize; + private readonly IMetaBuilder _metaBuilder; + private readonly Type _primaryResourceType; + private readonly ILinkBuilder _linkBuilder; + private readonly IIncludedResourceObjectBuilder _includedBuilder; + + public ResponseSerializer(IMetaBuilder metaBuilder, + ILinkBuilder linkBuilder, + IIncludedResourceObjectBuilder includedBuilder, + IFieldsToSerialize fieldsToSerialize, + IResourceObjectBuilder resourceObjectBuilder, + IResourceContextProvider provider) : + base(resourceObjectBuilder, provider) + { + _fieldsToSerialize = fieldsToSerialize; + _linkBuilder = linkBuilder; + _metaBuilder = metaBuilder; + _includedBuilder = includedBuilder; + _primaryResourceType = typeof(TResource); + } + + /// + public string Serialize(object data) + { + if (data is ErrorCollection error) + return error.GetJson(); + if (data is IEnumerable entities) + return SerializeMany(entities); + return SerializeSingle((IIdentifiable)data); + } + + /// + /// Convert a single entity into a serialized + /// + /// + /// This method is set internal instead of private for easier testability. + /// + internal string SerializeSingle(IIdentifiable entity) + { + if (RequestRelationship != null) + return JsonConvert.SerializeObject(((ResponseResourceObjectBuilder)_resourceObjectBuilder).Build(entity, RequestRelationship)); + + var (attributes, relationships) = GetFieldsToSerialize(); + var document = Build(entity, attributes, relationships); + var resourceObject = document.SingleData; + if (resourceObject != null) + resourceObject.Links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); + + AddTopLevelObjects(document); + return JsonConvert.SerializeObject(document); + + } + + private (List, List) GetFieldsToSerialize() + { + return (GetAttributesToSerialize(_primaryResourceType), GetRelationshipsToSerialize(_primaryResourceType)); + } + + /// + /// Convert a list of entities into a serialized + /// + /// + /// This method is set internal instead of private for easier testability. + /// + internal string SerializeMany(IEnumerable entities) + { + var (attributes, relationships) = GetFieldsToSerialize(); + var document = Build(entities, attributes, relationships); + foreach (ResourceObject resourceObject in (IEnumerable)document.Data) + { + var links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); + if (links == null) + break; + + resourceObject.Links = links; + } + + AddTopLevelObjects(document); + return JsonConvert.SerializeObject(document); + } + + /// + /// Gets the list of attributes to serialize for the given . + /// Note that the choice omitting null-values is not handled here, + /// but in . + /// + /// Type of entity to be serialized + /// List of allowed attributes in the serialized result + private List GetAttributesToSerialize(Type resourceType) + { + /// Check the attributes cache to see if the allowed attrs for this resource type were determined before. + if (_attributesToSerializeCache.TryGetValue(resourceType, out List allowedAttributes)) + return allowedAttributes; + + // Get the list of attributes to be exposed for this type + allowedAttributes = _fieldsToSerialize.GetAllowedAttributes(resourceType); + + // add to cache so we we don't have to look this up next time. + _attributesToSerializeCache.Add(resourceType, allowedAttributes); + return allowedAttributes; + } + + /// + /// By default, the server serializer exposes all defined relationships, unless + /// in the a subset to hide was defined explicitly. + /// + /// Type of entity to be serialized + /// List of allowed relationships in the serialized result + private List GetRelationshipsToSerialize(Type resourceType) + { + /// Check the relationships cache to see if the allowed attrs for this resource type were determined before. + if (_relationshipsToSerializeCache.TryGetValue(resourceType, out List allowedRelations)) + return allowedRelations; + + // Get the list of relationships to be exposed for this type + allowedRelations = _fieldsToSerialize.GetAllowedRelationships(resourceType); + // add to cache so we we don't have to look this up next time. + _relationshipsToSerializeCache.Add(resourceType, allowedRelations); + return allowedRelations; + + } + + /// + /// Adds top-level objects that are only added to a document in the case + /// of server-side serialization. + /// + private void AddTopLevelObjects(Document document) + { + document.Links = _linkBuilder.GetTopLevelLinks(_provider.GetResourceContext()); + document.Meta = _metaBuilder.GetMeta(); + document.Included = _includedBuilder.Build(); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializerFactory.cs b/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializerFactory.cs new file mode 100644 index 0000000000..9b39778d45 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializerFactory.cs @@ -0,0 +1,49 @@ +using System; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Serialization.Server +{ + /// + /// A factory class to abstract away the initialization of the serializer from the + /// .net core formatter pipeline. + /// + public class ResponseSerializerFactory : IJsonApiSerializerFactory + { + private readonly IServiceProvider _provider; + private readonly ICurrentRequest _currentRequest; + + public ResponseSerializerFactory(ICurrentRequest currentRequest, IScopedServiceProvider provider) + { + _currentRequest = currentRequest; + _provider = provider; + } + + /// + /// Initializes the server serializer using the + /// associated with the current request. + /// + public IJsonApiSerializer GetSerializer() + { + var targetType = GetDocumentPrimaryType(); + if (targetType == null) + return null; + + var serializerType = typeof(ResponseSerializer<>).MakeGenericType(targetType); + var serializer = (IResponseSerializer)_provider.GetService(serializerType); + if (_currentRequest.RequestRelationship != null && _currentRequest.IsRelationshipPath) + serializer.RequestRelationship = _currentRequest.RequestRelationship; + + return (IJsonApiSerializer)serializer; + } + + private Type GetDocumentPrimaryType() + { + if (_currentRequest.RequestRelationship != null && !_currentRequest.IsRelationshipPath) + return _currentRequest.RequestRelationship.RightType; + + return _currentRequest.GetRequestResource()?.ResourceType; + } + } +} diff --git a/src/JsonApiDotNetCore/Services/Contract/IGetRelationshipsService.cs b/src/JsonApiDotNetCore/Services/Contract/IGetRelationshipsService.cs index e519d0b4d1..9597a88830 100644 --- a/src/JsonApiDotNetCore/Services/Contract/IGetRelationshipsService.cs +++ b/src/JsonApiDotNetCore/Services/Contract/IGetRelationshipsService.cs @@ -10,6 +10,6 @@ public interface IGetRelationshipsService : IGetRelationshipsService public interface IGetRelationshipsService where T : class, IIdentifiable { - Task GetRelationshipsAsync(TId id, string relationshipName); + Task GetRelationshipsAsync(TId id, string relationshipName); } } diff --git a/src/JsonApiDotNetCore/Services/Contract/IRequestMeta.cs b/src/JsonApiDotNetCore/Services/Contract/IRequestMeta.cs new file mode 100644 index 0000000000..3083acdfe1 --- /dev/null +++ b/src/JsonApiDotNetCore/Services/Contract/IRequestMeta.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Services +{ + /// + /// Service to add global top-level metadata to a . + /// Use on + /// to specify top-level metadata per resource type. + /// + public interface IRequestMeta + { + Dictionary GetMeta(); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Services/Contract/IResourceDefinitionProvider.cs b/src/JsonApiDotNetCore/Services/Contract/IResourceDefinitionProvider.cs new file mode 100644 index 0000000000..7f25d22470 --- /dev/null +++ b/src/JsonApiDotNetCore/Services/Contract/IResourceDefinitionProvider.cs @@ -0,0 +1,19 @@ +using System; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Query +{ + /// + /// Retrieves a from the DI container. + /// Abstracts away the creation of the corresponding generic type and usage + /// of the service provider to do so. + /// + public interface IResourceDefinitionProvider + { + /// + /// Retrieves the resource definition associated to . + /// + /// + IResourceDefinition Get(Type resourceType); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Services/Contract/IUpdateRelationshipService.cs b/src/JsonApiDotNetCore/Services/Contract/IUpdateRelationshipService.cs index a942cc0f74..188e827701 100644 --- a/src/JsonApiDotNetCore/Services/Contract/IUpdateRelationshipService.cs +++ b/src/JsonApiDotNetCore/Services/Contract/IUpdateRelationshipService.cs @@ -11,6 +11,6 @@ public interface IUpdateRelationshipService : IUpdateRelationshipService where T : class, IIdentifiable { - Task UpdateRelationshipsAsync(TId id, string relationshipName, List relationships); + Task UpdateRelationshipsAsync(TId id, string relationshipName, object relationships); } } diff --git a/src/JsonApiDotNetCore/Services/ControllerContext.cs b/src/JsonApiDotNetCore/Services/ControllerContext.cs deleted file mode 100644 index 1984262b15..0000000000 --- a/src/JsonApiDotNetCore/Services/ControllerContext.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Reflection; -using JsonApiDotNetCore.Internal; - -namespace JsonApiDotNetCore.Services -{ - public interface IControllerContext - { - Type ControllerType { get; set; } - ContextEntity RequestEntity { get; set; } - TAttribute GetControllerAttribute() where TAttribute : Attribute; - } - - public class ControllerContext : IControllerContext - { - public Type ControllerType { get; set; } - public ContextEntity RequestEntity { get; set; } - - public TAttribute GetControllerAttribute() where TAttribute : Attribute - { - var attribute = ControllerType.GetTypeInfo().GetCustomAttribute(typeof(TAttribute)); - return attribute == null ? null : (TAttribute)attribute; - } - } -} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Services/DefaultResourceService.cs b/src/JsonApiDotNetCore/Services/DefaultResourceService.cs new file mode 100644 index 0000000000..ea6f4c7a87 --- /dev/null +++ b/src/JsonApiDotNetCore/Services/DefaultResourceService.cs @@ -0,0 +1,336 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Hooks; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Query; +using JsonApiDotNetCore.Extensions; + +namespace JsonApiDotNetCore.Services +{ + /// + /// Entity mapping class + /// + /// + /// + public class DefaultResourceService : + IResourceService + where TResource : class, IIdentifiable + { + private readonly IPageService _pageManager; + private readonly IJsonApiOptions _options; + private readonly IFilterService _filterService; + private readonly ISortService _sortService; + private readonly IResourceRepository _repository; + private readonly ILogger _logger; + private readonly IResourceHookExecutor _hookExecutor; + private readonly IIncludeService _includeService; + private readonly ISparseFieldsService _sparseFieldsService; + private readonly ResourceContext _currentRequestResource; + + public DefaultResourceService( + IEnumerable queryParameters, + IJsonApiOptions options, + IResourceRepository repository, + IResourceContextProvider provider, + IResourceHookExecutor hookExecutor = null, + ILoggerFactory loggerFactory = null) + { + _includeService = queryParameters.FirstOrDefault(); + _sparseFieldsService = queryParameters.FirstOrDefault(); + _pageManager = queryParameters.FirstOrDefault(); + _sortService = queryParameters.FirstOrDefault(); + _filterService = queryParameters.FirstOrDefault(); + _options = options; + _repository = repository; + _hookExecutor = hookExecutor; + _logger = loggerFactory?.CreateLogger>(); + _currentRequestResource = provider.GetResourceContext(); + } + + + public virtual async Task CreateAsync(TResource entity) + { + entity = IsNull(_hookExecutor) ? entity : _hookExecutor.BeforeCreate(AsList(entity), ResourcePipeline.Post).SingleOrDefault(); + entity = await _repository.CreateAsync(entity); + + if (_includeService.Get().Any()) + entity = await GetWithRelationshipsAsync(entity.Id); + + if (!IsNull(_hookExecutor, entity)) + { + _hookExecutor.AfterCreate(AsList(entity), ResourcePipeline.Post); + entity = _hookExecutor.OnReturn(AsList(entity), ResourcePipeline.Get).SingleOrDefault(); + } + return entity; + } + + public virtual async Task DeleteAsync(TId id) + { + var entity = (TResource)Activator.CreateInstance(typeof(TResource)); + entity.Id = id; + if (!IsNull(_hookExecutor, entity)) _hookExecutor.BeforeDelete(AsList(entity), ResourcePipeline.Delete); + var succeeded = await _repository.DeleteAsync(entity.Id); + if (!IsNull(_hookExecutor, entity)) _hookExecutor.AfterDelete(AsList(entity), ResourcePipeline.Delete, succeeded); + return succeeded; + } + + public virtual async Task> GetAsync() + { + _hookExecutor?.BeforeRead(ResourcePipeline.Get); + + var entityQuery = _repository.Get(); + entityQuery = ApplyFilter(entityQuery); + entityQuery = ApplySort(entityQuery); + entityQuery = ApplyInclude(entityQuery); + entityQuery = ApplySelect(entityQuery); + + if (!IsNull(_hookExecutor, entityQuery)) + { + var entities = await _repository.ToListAsync(entityQuery); + _hookExecutor.AfterRead(entities, ResourcePipeline.Get); + entityQuery = _hookExecutor.OnReturn(entities, ResourcePipeline.Get).AsQueryable(); + } + + if (_options.IncludeTotalRecordCount) + _pageManager.TotalRecords = await _repository.CountAsync(entityQuery); + + // pagination should be done last since it will execute the query + var pagedEntities = await ApplyPageQueryAsync(entityQuery); + return pagedEntities; + } + + public virtual async Task GetAsync(TId id) + { + var pipeline = ResourcePipeline.GetSingle; + _hookExecutor?.BeforeRead(pipeline, id.ToString()); + + var entityQuery = _repository.Get(id); + entityQuery = ApplyInclude(entityQuery); + entityQuery = ApplySelect(entityQuery); + var entity = await _repository.FirstOrDefaultAsync(entityQuery); + + if (!IsNull(_hookExecutor, entity)) + { + _hookExecutor.AfterRead(AsList(entity), pipeline); + entity = _hookExecutor.OnReturn(AsList(entity), pipeline).SingleOrDefault(); + } + return entity; + } + + // triggered by GET /articles/1/relationships/{relationshipName} + public virtual async Task GetRelationshipsAsync(TId id, string relationshipName) + { + var relationship = GetRelationship(relationshipName); + + // BeforeRead hook execution + _hookExecutor?.BeforeRead(ResourcePipeline.GetRelationship, id.ToString()); + + // TODO: it would be better if we could distinguish whether or not the relationship was not found, + // vs the relationship not being set on the instance of T + var entityQuery = _repository.Include(_repository.Get(id), new RelationshipAttribute[] { relationship }); + var entity = await _repository.FirstOrDefaultAsync(entityQuery); + if (entity == null) // this does not make sense. If the parent entity is not found, this error is thrown? + throw new JsonApiException(404, $"Relationship '{relationshipName}' not found."); + + if (!IsNull(_hookExecutor, entity)) + { // AfterRead and OnReturn resource hook execution. + _hookExecutor.AfterRead(AsList(entity), ResourcePipeline.GetRelationship); + entity = _hookExecutor.OnReturn(AsList(entity), ResourcePipeline.GetRelationship).SingleOrDefault(); + } + + return entity; + } + + // triggered by GET /articles/1/{relationshipName} + public virtual async Task GetRelationshipAsync(TId id, string relationshipName) + { + var relationship = GetRelationship(relationshipName); + var resource = await GetRelationshipsAsync(id, relationshipName); + return relationship.GetValue(resource); + } + + public virtual async Task UpdateAsync(TId id, TResource entity) + { + entity = IsNull(_hookExecutor) ? entity : _hookExecutor.BeforeUpdate(AsList(entity), ResourcePipeline.Patch).SingleOrDefault(); + entity = await _repository.UpdateAsync(entity); + if (!IsNull(_hookExecutor, entity)) + { + _hookExecutor.AfterUpdate(AsList(entity), ResourcePipeline.Patch); + entity = _hookExecutor.OnReturn(AsList(entity), ResourcePipeline.Patch).SingleOrDefault(); + } + return entity; + } + + // triggered by PATCH /articles/1/relationships/{relationshipName} + public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipName, object related) + { + var relationship = GetRelationship(relationshipName); + var entityQuery = _repository.Include(_repository.Get(id), new RelationshipAttribute[] { relationship }); + var entity = await _repository.FirstOrDefaultAsync(entityQuery); + if (entity == null) + throw new JsonApiException(404, $"Entity with id {id} could not be found."); + + entity = IsNull(_hookExecutor) ? entity : _hookExecutor.BeforeUpdate(AsList(entity), ResourcePipeline.PatchRelationship).SingleOrDefault(); + + string[] relationshipIds = null; + if (related != null) + { + if (relationship is HasOneAttribute) + relationshipIds = new string[] { ((IIdentifiable)related).StringId }; + else + relationshipIds = ((IEnumerable)related).Select(e => e.StringId).ToArray(); + } + + await _repository.UpdateRelationshipsAsync(entity, relationship, relationshipIds ?? new string[0] ); + + if (!IsNull(_hookExecutor, entity)) _hookExecutor.AfterUpdate(AsList(entity), ResourcePipeline.PatchRelationship); + } + + protected virtual async Task> ApplyPageQueryAsync(IQueryable entities) + { + if (!(_pageManager.PageSize > 0)) + { + var allEntities = await _repository.ToListAsync(entities); + return allEntities as IEnumerable; + } + + if (_logger?.IsEnabled(LogLevel.Information) == true) + { + _logger?.LogInformation($"Applying paging query. Fetching page {_pageManager.CurrentPage} " + + $"with {_pageManager.PageSize} entities"); + } + + return await _repository.PageAsync(entities, _pageManager.PageSize, _pageManager.CurrentPage); + } + + /// + /// Applies sort queries + /// + /// + /// + protected virtual IQueryable ApplySort(IQueryable entities) + { + var queries = _sortService.Get(); + if (queries != null && queries.Any()) + foreach (var query in queries) + entities = _repository.Sort(entities, query); + + return entities; + } + + /// + /// Applies filter queries + /// + /// + /// + protected virtual IQueryable ApplyFilter(IQueryable entities) + { + var queries = _filterService.Get(); + if (queries != null && queries.Any()) + foreach (var query in queries) + entities = _repository.Filter(entities, query); + + return entities; + } + + + /// + /// Applies include queries + /// + /// + /// + protected virtual IQueryable ApplyInclude(IQueryable entities) + { + var chains = _includeService.Get(); + if (chains != null && chains.Any()) + foreach (var r in chains) + entities = _repository.Include(entities, r.ToArray()); + + return entities; + } + + /// + /// Applies sparse field selection queries + /// + /// + /// + protected virtual IQueryable ApplySelect(IQueryable entities) + { + var fields = _sparseFieldsService.Get(); + if (fields != null && fields.Any()) + entities = _repository.Select(entities, fields.ToArray()); + + return entities; + } + + /// + /// Get the specified id with relationships provided in the post request + /// + /// i + /// + private async Task GetWithRelationshipsAsync(TId id) + { + var sparseFieldset = _sparseFieldsService.Get(); + var query = _repository.Select(_repository.Get(id), sparseFieldset.ToArray()); + + foreach (var chain in _includeService.Get()) + query = _repository.Include(query, chain.ToArray()); + + TResource value; + // https://github.com/aspnet/EntityFrameworkCore/issues/6573 + if (sparseFieldset.Any()) + value = query.FirstOrDefault(); + else + value = await _repository.FirstOrDefaultAsync(query); + + + return value; + } + + private bool IsNull(params object[] values) + { + foreach (var val in values) + { + if (val == null) return true; + } + return false; + } + + private RelationshipAttribute GetRelationship(string relationshipName) + { + var relationship = _currentRequestResource.Relationships.Single(r => r.Is(relationshipName)); + if (relationship == null) + throw new JsonApiException(422, $"Relationship '{relationshipName}' does not exist on resource '{typeof(TResource)}'."); + return relationship; + } + + private List AsList(TResource entity) + { + return new List { entity }; + } + } + + /// + /// No mapping with integer as default + /// + /// + public class DefaultResourceService : DefaultResourceService, + IResourceService + where TResource : class, IIdentifiable + { + public DefaultResourceService(IEnumerable queryParameters, + IJsonApiOptions options, + IResourceRepository repository, + IResourceContextProvider provider, + IResourceHookExecutor hookExecutor = null, + ILoggerFactory loggerFactory = null) + : base(queryParameters, options, repository, provider, hookExecutor, loggerFactory) { } + } +} diff --git a/src/JsonApiDotNetCore/Services/EntityResourceService.cs b/src/JsonApiDotNetCore/Services/EntityResourceService.cs deleted file mode 100644 index c889af68b8..0000000000 --- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs +++ /dev/null @@ -1,364 +0,0 @@ -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Hooks; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace JsonApiDotNetCore.Services -{ - public class EntityResourceService : EntityResourceService, - IResourceService - where TResource : class, IIdentifiable - { - public EntityResourceService( - IJsonApiContext jsonApiContext, - IEntityRepository entityRepository, - ILoggerFactory loggerFactory = null, - IResourceHookExecutor hookExecutor = null) : - base(jsonApiContext, entityRepository, loggerFactory, hookExecutor) - { } - } - - public class EntityResourceService : EntityResourceService, - IResourceService - where TResource : class, IIdentifiable - { - public EntityResourceService( - IJsonApiContext jsonApiContext, - IEntityRepository entityRepository, - ILoggerFactory loggerFactory = null, - IResourceHookExecutor hookExecutor = null) : - base(jsonApiContext, entityRepository, hookExecutor, loggerFactory) - { } - } - - public class EntityResourceService : - IResourceService - where TResource : class, IIdentifiable - where TEntity : class, IIdentifiable - { - private readonly IJsonApiContext _jsonApiContext; - private readonly IEntityRepository _entities; - private readonly ILogger _logger; - private readonly IResourceMapper _mapper; - private readonly IResourceHookExecutor _hookExecutor; - - - public EntityResourceService( - IJsonApiContext jsonApiContext, - IEntityRepository entityRepository, - IResourceHookExecutor hookExecutor = null, - IResourceMapper mapper = null, - ILoggerFactory loggerFactory = null) - { - _jsonApiContext = jsonApiContext; - _entities = entityRepository; - - if (mapper == null && typeof(TResource) != typeof(TEntity)) - { - throw new InvalidOperationException("Resource and Entity types are NOT the same. Please provide a mapper."); - } - _hookExecutor = hookExecutor; - _mapper = mapper; - _logger = loggerFactory?.CreateLogger>(); - } - - - public EntityResourceService( - IJsonApiContext jsonApiContext, - IEntityRepository entityRepository, - IResourceHookExecutor hookExecutor, - ILoggerFactory loggerFactory = null) : this(jsonApiContext, entityRepository, hookExecutor: hookExecutor, mapper: null, loggerFactory: null) - { } - - public EntityResourceService( - IJsonApiContext jsonApiContext, - IEntityRepository entityRepository, - ILoggerFactory loggerFactory = null) : this(jsonApiContext, entityRepository, hookExecutor: null, mapper: null, loggerFactory: loggerFactory) - { } - - [Obsolete("Use ctor with signature (jsonApiContext, entityRepository, hookExecutor = null, mapper = null, loggerFactory = null")] - public EntityResourceService( - IJsonApiContext jsonApiContext, - IEntityRepository entityRepository, - ILoggerFactory loggerFactory, - IResourceMapper mapper ) : this(jsonApiContext, entityRepository, hookExecutor: null, mapper: mapper, loggerFactory: loggerFactory) - { } - - public virtual async Task CreateAsync(TResource resource) - { - var entity = MapIn(resource); - - entity = IsNull(_hookExecutor) ? entity : _hookExecutor.BeforeCreate(AsList(entity), ResourcePipeline.Post).SingleOrDefault(); - entity = await _entities.CreateAsync(entity); - - // this ensures relationships get reloaded from the database if they have - // been requested - // https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/343 - if (ShouldIncludeRelationships()) - { - if (_entities is IEntityFrameworkRepository efRepository) - efRepository.DetachRelationshipPointers(entity); - - entity = await GetWithRelationshipsAsync(entity.Id); - - } - if (!IsNull(_hookExecutor, entity)) - { - _hookExecutor.AfterCreate(AsList(entity), ResourcePipeline.Post); - entity = _hookExecutor.OnReturn(AsList(entity), ResourcePipeline.Get).SingleOrDefault(); - } - return MapOut(entity); - } - public virtual async Task DeleteAsync(TId id) - { - var entity = (TEntity)Activator.CreateInstance(typeof(TEntity)); - entity.Id = id; - if (!IsNull(_hookExecutor, entity)) _hookExecutor.BeforeDelete(AsList(entity), ResourcePipeline.Delete); - var succeeded = await _entities.DeleteAsync(entity.Id); - if (!IsNull(_hookExecutor, entity)) _hookExecutor.AfterDelete(AsList(entity), ResourcePipeline.Delete, succeeded); - return succeeded; - } - - public virtual async Task> GetAsync() - { - _hookExecutor?.BeforeRead(ResourcePipeline.Get); - var entities = _entities.Get(); - - entities = ApplySortAndFilterQuery(entities); - - if (ShouldIncludeRelationships()) - entities = IncludeRelationships(entities, _jsonApiContext.QuerySet.IncludedRelationships); - - if (_jsonApiContext.Options.IncludeTotalRecordCount) - _jsonApiContext.PageManager.TotalRecords = await _entities.CountAsync(entities); - - entities = _entities.Select(entities, _jsonApiContext.QuerySet?.Fields); - - if (!IsNull(_hookExecutor, entities)) - { - var result = entities.ToList(); - _hookExecutor.AfterRead(result, ResourcePipeline.Get); - entities = _hookExecutor.OnReturn(result, ResourcePipeline.Get).AsQueryable(); - } - - if (_jsonApiContext.Options.IncludeTotalRecordCount) - _jsonApiContext.PageManager.TotalRecords = await _entities.CountAsync(entities); - - // pagination should be done last since it will execute the query - var pagedEntities = await ApplyPageQueryAsync(entities); - return pagedEntities; - } - - public virtual async Task GetAsync(TId id) - { - var pipeline = ResourcePipeline.GetSingle; - _hookExecutor?.BeforeRead(pipeline, id.ToString()); - TEntity entity; - if (ShouldIncludeRelationships()) - { - entity = await GetWithRelationshipsAsync(id); - } - else - { - entity = await _entities.GetAsync(id); - } - if(!IsNull(_hookExecutor, entity)) - { - _hookExecutor.AfterRead(AsList(entity), pipeline); - entity = _hookExecutor.OnReturn(AsList(entity), pipeline).SingleOrDefault(); - } - return MapOut(entity); - - } - - // triggered by GET /articles/1/relationships/{relationshipName} - public virtual async Task GetRelationshipsAsync(TId id, string relationshipName) => await GetRelationshipAsync(id, relationshipName); - - // triggered by GET /articles/1/{relationshipName} - public virtual async Task GetRelationshipAsync(TId id, string relationshipName) - { - - _hookExecutor?.BeforeRead(ResourcePipeline.GetRelationship, id.ToString()); - var entity = await _entities.GetAndIncludeAsync(id, relationshipName); - if (!IsNull(_hookExecutor, entity)) - { - _hookExecutor.AfterRead(AsList(entity), ResourcePipeline.GetRelationship); - entity = _hookExecutor.OnReturn(AsList(entity), ResourcePipeline.GetRelationship).SingleOrDefault(); - } - - // TODO: it would be better if we could distinguish whether or not the relationship was not found, - // vs the relationship not being set on the instance of T - if (entity == null) - { - throw new JsonApiException(404, $"Relationship '{relationshipName}' not found."); - } - - var resource = MapOut(entity); - - // compound-property -> CompoundProperty - var navigationPropertyName = _jsonApiContext.ResourceGraph.GetRelationshipName(relationshipName); - if (navigationPropertyName == null) - throw new JsonApiException(422, $"Relationship '{relationshipName}' does not exist on resource '{typeof(TResource)}'."); - - var relationshipValue = _jsonApiContext.ResourceGraph.GetRelationship(resource, navigationPropertyName); - return relationshipValue; - } - - public virtual async Task UpdateAsync(TId id, TResource resource) - { - var entity = MapIn(resource); - - entity = IsNull(_hookExecutor) ? entity : _hookExecutor.BeforeUpdate(AsList(entity), ResourcePipeline.Patch).SingleOrDefault(); - entity = await _entities.UpdateAsync(entity); - if (!IsNull(_hookExecutor, entity)) - { - _hookExecutor.AfterUpdate(AsList(entity), ResourcePipeline.Patch); - entity = _hookExecutor.OnReturn(AsList(entity), ResourcePipeline.Patch).SingleOrDefault(); - } - return MapOut(entity); - } - - // triggered by PATCH /articles/1/relationships/{relationshipName} - public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipName, List relationships) - { - var entity = await _entities.GetAndIncludeAsync(id, relationshipName); - if (entity == null) - { - throw new JsonApiException(404, $"Entity with id {id} could not be found."); - } - - var relationship = _jsonApiContext.ResourceGraph - .GetContextEntity(typeof(TResource)) - .Relationships - .FirstOrDefault(r => r.Is(relationshipName)); - - var relationshipType = relationship.DependentType; - - // update relationship type with internalname - var entityProperty = typeof(TEntity).GetProperty(relationship.InternalRelationshipName); - if (entityProperty == null) - { - throw new JsonApiException(404, $"Property {relationship.InternalRelationshipName} " + - $"could not be found on entity."); - } - - /// Why are we changing this value on the attribute and setting it back below? This feels very hacky - relationship.Type = relationship.IsHasMany - ? entityProperty.PropertyType.GetGenericArguments()[0] - : entityProperty.PropertyType; - - var relationshipIds = relationships.Select(r => r?.Id?.ToString()); - - entity = IsNull(_hookExecutor) ? entity : _hookExecutor.BeforeUpdate(AsList(entity), ResourcePipeline.PatchRelationship).SingleOrDefault(); - await _entities.UpdateRelationshipsAsync(entity, relationship, relationshipIds); - if (!IsNull(_hookExecutor, entity)) _hookExecutor.AfterUpdate(AsList(entity), ResourcePipeline.PatchRelationship); - - relationship.Type = relationshipType; - } - - protected virtual async Task> ApplyPageQueryAsync(IQueryable entities) - { - var pageManager = _jsonApiContext.PageManager; - if (!pageManager.IsPaginated) - { - var allEntities = await _entities.ToListAsync(entities); - return (typeof(TResource) == typeof(TEntity)) ? allEntities as IEnumerable : - _mapper.Map>(allEntities); - } - - if (_logger?.IsEnabled(LogLevel.Information) == true) - { - _logger?.LogInformation($"Applying paging query. Fetching page {pageManager.CurrentPage} " + - $"with {pageManager.PageSize} entities"); - } - - var pagedEntities = await _entities.PageAsync(entities, pageManager.PageSize, pageManager.CurrentPage); - - return MapOut(pagedEntities); - } - - protected virtual IQueryable ApplySortAndFilterQuery(IQueryable entities) - { - var query = _jsonApiContext.QuerySet; - - if (_jsonApiContext.QuerySet == null) - return entities; - - if (query.Filters.Count > 0) - foreach (var filter in query.Filters) - entities = _entities.Filter(entities, filter); - - entities = _entities.Sort(entities, query.SortParameters); - - return entities; - } - - protected virtual IQueryable IncludeRelationships(IQueryable entities, List relationships) - { - _jsonApiContext.IncludedRelationships = relationships; - - foreach (var r in relationships) - entities = _entities.Include(entities, r); - - return entities; - } - - private async Task GetWithRelationshipsAsync(TId id) - { - var query = _entities.Select(_entities.Get(), _jsonApiContext.QuerySet?.Fields).Where(e => e.Id.Equals(id)); - - _jsonApiContext.QuerySet.IncludedRelationships.ForEach(r => - { - query = _entities.Include(query, r); - }); - - TEntity value; - // https://github.com/aspnet/EntityFrameworkCore/issues/6573 - if (_jsonApiContext.QuerySet?.Fields?.Count > 0) - value = query.FirstOrDefault(); - else - value = await _entities.FirstOrDefaultAsync(query); - - return value; - } - - - private bool IsNull(params object[] values) - { - foreach (var val in values) - { - if (val == null) return true; - } - return false; - } - - private bool ShouldIncludeRelationships() - => (_jsonApiContext.QuerySet?.IncludedRelationships != null && - _jsonApiContext.QuerySet.IncludedRelationships.Count > 0); - - private TResource MapOut(TEntity entity) - => (typeof(TResource) == typeof(TEntity)) - ? entity as TResource : - _mapper.Map(entity); - - private IEnumerable MapOut(IEnumerable entities) - => (typeof(TResource) == typeof(TEntity)) - ? entities as IEnumerable - : _mapper.Map>(entities); - - private TEntity MapIn(TResource resource) - => (typeof(TResource) == typeof(TEntity)) - ? resource as TEntity - : _mapper.Map(resource); - - private List AsList(TEntity entity) - { - return new List { entity }; - } - } -} diff --git a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs deleted file mode 100644 index d3fb014bcc..0000000000 --- a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System; -using System.Collections.Generic; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Generics; -using JsonApiDotNetCore.Internal.Query; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Request; - -namespace JsonApiDotNetCore.Services -{ - public interface IJsonApiApplication - { - JsonApiOptions Options { get; set; } - IResourceGraph ResourceGraph { get; set; } - } - - public interface IQueryRequest - { - List IncludedRelationships { get; set; } - QuerySet QuerySet { get; set; } - PageManager PageManager { get; set; } - } - - public interface IUpdateRequest - { - /// - /// The attributes that were included in a PATCH request. - /// Only the attributes in this dictionary should be updated. - /// - Dictionary AttributesToUpdate { get; set; } - - /// - /// Any relationships that were included in a PATCH request. - /// Only the relationships in this dictionary should be updated. - /// - Dictionary RelationshipsToUpdate { get; } - } - - public interface IJsonApiRequest : IJsonApiApplication, IUpdateRequest, IQueryRequest - { - /// - /// The request namespace. This may be an absolute or relative path - /// depending upon the configuration. - /// - /// - /// Absolute: https://example.com/api/v1 - /// - /// Relative: /api/v1 - /// - string BasePath { get; set; } - - /// - /// Stores information to set relationships for the request resource. - /// These relationships must already exist and should not be re-created. - /// By default, it is the responsibility of the repository to use the - /// relationship pointers to persist the relationship. - /// - /// The expected use case is POST-ing or PATCH-ing an entity with HasMany - /// relaitonships: - /// - /// { - /// "data": { - /// "type": "photos", - /// "attributes": { - /// "title": "Ember Hamster", - /// "src": "http://example.com/images/productivity.png" - /// }, - /// "relationships": { - /// "tags": { - /// "data": [ - /// { "type": "tags", "id": "2" }, - /// { "type": "tags", "id": "3" } - /// ] - /// } - /// } - /// } - /// } - /// - /// - HasManyRelationshipPointers HasManyRelationshipPointers { get; } - - /// - /// Stores information to set relationships for the request resource. - /// These relationships must already exist and should not be re-created. - /// - /// The expected use case is POST-ing or PATCH-ing - /// an entity with HasOne relationships: - /// - /// { - /// "data": { - /// "type": "photos", - /// "attributes": { - /// "title": "Ember Hamster", - /// "src": "http://example.com/images/productivity.png" - /// }, - /// "relationships": { - /// "photographer": { - /// "data": { "type": "people", "id": "2" } - /// } - /// } - /// } - /// } - /// - /// - HasOneRelationshipPointers HasOneRelationshipPointers { get; } - - /// - /// If the request is a bulk json:api v1.1 operations request. - /// This is determined by the ` - /// ` class. - /// - /// See [json-api/1254](https://github.com/json-api/json-api/pull/1254) for details. - /// - bool IsBulkOperationRequest { get; set; } - - /// - /// The ``for the target route. - /// - /// - /// - /// For a `GET /articles` request, `RequestEntity` will be set - /// to the `Article` resource representation on the `JsonApiContext`. - /// - ContextEntity RequestEntity { get; set; } - - /// - /// The concrete type of the controller that was activated by the MVC routing middleware - /// - Type ControllerType { get; set; } - - /// - /// The json:api meta data at the document level - /// - Dictionary DocumentMeta { get; set; } - - /// - /// If the request is on the `{id}/relationships/{relationshipName}` route - /// - bool IsRelationshipPath { get; } - } - - public interface IJsonApiContext : IJsonApiRequest - { - IJsonApiContext ApplyContext(object controller); - IMetaBuilder MetaBuilder { get; set; } - IGenericProcessorFactory GenericProcessorFactory { get; set; } - - /// - /// **_Experimental_**: do not use. It is likely to change in the future. - /// - /// Resets operational state information. - /// - void BeginOperation(); - } -} diff --git a/src/JsonApiDotNetCore/Services/IRequestMeta.cs b/src/JsonApiDotNetCore/Services/IRequestMeta.cs deleted file mode 100644 index 7dd5fdcada..0000000000 --- a/src/JsonApiDotNetCore/Services/IRequestMeta.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace JsonApiDotNetCore.Services -{ - public interface IRequestMeta - { - Dictionary GetMeta(); - } -} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs deleted file mode 100644 index 495354b1e0..0000000000 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Generics; -using JsonApiDotNetCore.Internal.Query; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Request; -using Microsoft.AspNetCore.Http; - -namespace JsonApiDotNetCore.Services -{ - public class JsonApiContext : IJsonApiContext - { - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IQueryParser _queryParser; - private readonly IControllerContext _controllerContext; - - public JsonApiContext( - IResourceGraph resourceGraph, - IHttpContextAccessor httpContextAccessor, - JsonApiOptions options, - IMetaBuilder metaBuilder, - IGenericProcessorFactory genericProcessorFactory, - IQueryParser queryParser, - IControllerContext controllerContext) - { - ResourceGraph = resourceGraph; - _httpContextAccessor = httpContextAccessor; - Options = options; - MetaBuilder = metaBuilder; - GenericProcessorFactory = genericProcessorFactory; - _queryParser = queryParser; - _controllerContext = controllerContext; - } - - public JsonApiOptions Options { get; set; } - public IResourceGraph ResourceGraph { get; set; } - [Obsolete("Use the proxied member IControllerContext.RequestEntity instead.")] - public ContextEntity RequestEntity { get => _controllerContext.RequestEntity; set => _controllerContext.RequestEntity = value; } - public string BasePath { get; set; } - public QuerySet QuerySet { get; set; } - public bool IsRelationshipData { get; set; } - public bool IsRelationshipPath { get; private set; } - public List IncludedRelationships { get; set; } - public PageManager PageManager { get; set; } - public IMetaBuilder MetaBuilder { get; set; } - public IGenericProcessorFactory GenericProcessorFactory { get; set; } - public Type ControllerType { get; set; } - public Dictionary DocumentMeta { get; set; } - public bool IsBulkOperationRequest { get; set; } - - public Dictionary AttributesToUpdate { get; set; } = new Dictionary(); - public Dictionary RelationshipsToUpdate { get => GetRelationshipsToUpdate(); } - - private Dictionary GetRelationshipsToUpdate() - { - var hasOneEntries = HasOneRelationshipPointers.Get().ToDictionary(kvp => (RelationshipAttribute)kvp.Key, kvp => (object)kvp.Value); - var hasManyEntries = HasManyRelationshipPointers.Get().ToDictionary(kvp => kvp.Key, kvp => (object)kvp.Value); - return hasOneEntries.Union(hasManyEntries).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - } - - public HasManyRelationshipPointers HasManyRelationshipPointers { get; private set; } = new HasManyRelationshipPointers(); - public HasOneRelationshipPointers HasOneRelationshipPointers { get; private set; } = new HasOneRelationshipPointers(); - - public IJsonApiContext ApplyContext(object controller) - { - if (controller == null) - throw new JsonApiException(500, $"Cannot ApplyContext from null controller for type {typeof(T)}"); - - _controllerContext.ControllerType = controller.GetType(); - _controllerContext.RequestEntity = ResourceGraph.GetContextEntity(typeof(T)); - if (_controllerContext.RequestEntity == null) - throw new JsonApiException(500, $"A resource has not been properly defined for type '{typeof(T)}'. Ensure it has been registered on the ResourceGraph."); - - var context = _httpContextAccessor.HttpContext; - - if (context.Request.Query.Count > 0) - { - QuerySet = _queryParser.Parse(context.Request.Query); - IncludedRelationships = QuerySet.IncludedRelationships; - } - - BasePath = new LinkBuilder(this).GetBasePath(context, _controllerContext.RequestEntity.EntityName); - PageManager = GetPageManager(); - IsRelationshipPath = PathIsRelationship(context.Request.Path.Value); - - return this; - } - - internal static bool PathIsRelationship(string requestPath) - { - // while(!Debugger.IsAttached) { Thread.Sleep(1000); } - const string relationships = "relationships"; - const char pathSegmentDelimiter = '/'; - - var span = requestPath.AsSpan(); - - // we need to iterate over the string, from the end, - // checking whether or not the 2nd to last path segment - // is "relationships" - // -2 is chosen in case the path ends with '/' - for (var i = requestPath.Length - 2; i >= 0; i--) - { - // if there are not enough characters left in the path to - // contain "relationships" - if (i < relationships.Length) - return false; - - // we have found the first instance of '/' - if (span[i] == pathSegmentDelimiter) - { - // in the case of a "relationships" route, the next - // path segment will be "relationships" - return ( - span.Slice(i - relationships.Length, relationships.Length) - .SequenceEqual(relationships.AsSpan()) - ); - } - } - - return false; - } - - private PageManager GetPageManager() - { - if (Options.DefaultPageSize == 0 && (QuerySet == null || QuerySet.PageQuery.PageSize == 0)) - return new PageManager(); - - var query = QuerySet?.PageQuery ?? new PageQuery(); - - return new PageManager - { - DefaultPageSize = Options.DefaultPageSize, - CurrentPage = query.PageOffset, - PageSize = query.PageSize > 0 ? query.PageSize : Options.DefaultPageSize - }; - } - - public void BeginOperation() - { - IncludedRelationships = new List(); - AttributesToUpdate = new Dictionary(); - HasManyRelationshipPointers = new HasManyRelationshipPointers(); - HasOneRelationshipPointers = new HasOneRelationshipPointers(); - } - } -} diff --git a/src/JsonApiDotNetCore/Services/Operations/IOpProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/IOpProcessor.cs deleted file mode 100644 index 0a2d30397c..0000000000 --- a/src/JsonApiDotNetCore/Services/Operations/IOpProcessor.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Threading.Tasks; -using JsonApiDotNetCore.Models.Operations; - -namespace JsonApiDotNetCore.Services.Operations -{ - public interface IOpProcessor - { - Task ProcessAsync(Operation operation); - } -} diff --git a/src/JsonApiDotNetCore/Services/Operations/OperationProcessorResolver.cs b/src/JsonApiDotNetCore/Services/Operations/OperationProcessorResolver.cs deleted file mode 100644 index d509340c0d..0000000000 --- a/src/JsonApiDotNetCore/Services/Operations/OperationProcessorResolver.cs +++ /dev/null @@ -1,114 +0,0 @@ -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Generics; -using JsonApiDotNetCore.Models.Operations; -using JsonApiDotNetCore.Services.Operations.Processors; - -namespace JsonApiDotNetCore.Services.Operations -{ - /// - /// Used to resolve at runtime based on the required operation - /// - public interface IOperationProcessorResolver - { - /// - /// Locates the correct - /// - IOpProcessor LocateCreateService(Operation operation); - - /// - /// Locates the correct - /// - IOpProcessor LocateGetService(Operation operation); - - /// - /// Locates the correct - /// - IOpProcessor LocateRemoveService(Operation operation); - - /// - /// Locates the correct - /// - IOpProcessor LocateUpdateService(Operation operation); - } - - /// - public class OperationProcessorResolver : IOperationProcessorResolver - { - private readonly IGenericProcessorFactory _processorFactory; - private readonly IJsonApiContext _context; - - /// - public OperationProcessorResolver( - IGenericProcessorFactory processorFactory, - IJsonApiContext context) - { - _processorFactory = processorFactory; - _context = context; - } - - /// - public IOpProcessor LocateCreateService(Operation operation) - { - var resource = operation.GetResourceTypeName(); - - var contextEntity = GetResourceMetadata(resource); - - var processor = _processorFactory.GetProcessor( - typeof(ICreateOpProcessor<,>), contextEntity.EntityType, contextEntity.IdentityType - ); - - return processor; - } - - /// - public IOpProcessor LocateGetService(Operation operation) - { - var resource = operation.GetResourceTypeName(); - - var contextEntity = GetResourceMetadata(resource); - - var processor = _processorFactory.GetProcessor( - typeof(IGetOpProcessor<,>), contextEntity.EntityType, contextEntity.IdentityType - ); - - return processor; - } - - /// - public IOpProcessor LocateRemoveService(Operation operation) - { - var resource = operation.GetResourceTypeName(); - - var contextEntity = GetResourceMetadata(resource); - - var processor = _processorFactory.GetProcessor( - typeof(IRemoveOpProcessor<,>), contextEntity.EntityType, contextEntity.IdentityType - ); - - return processor; - } - - /// - public IOpProcessor LocateUpdateService(Operation operation) - { - var resource = operation.GetResourceTypeName(); - - var contextEntity = GetResourceMetadata(resource); - - var processor = _processorFactory.GetProcessor( - typeof(IUpdateOpProcessor<,>), contextEntity.EntityType, contextEntity.IdentityType - ); - - return processor; - } - - private ContextEntity GetResourceMetadata(string resourceName) - { - var contextEntity = _context.ResourceGraph.GetContextEntity(resourceName); - if(contextEntity == null) - throw new JsonApiException(400, $"This API does not expose a resource of type '{resourceName}'."); - - return contextEntity; - } - } -} diff --git a/src/JsonApiDotNetCore/Services/Operations/OperationsProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/OperationsProcessor.cs deleted file mode 100644 index 275b4b85b0..0000000000 --- a/src/JsonApiDotNetCore/Services/Operations/OperationsProcessor.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Operations; -using Microsoft.EntityFrameworkCore; - -namespace JsonApiDotNetCore.Services.Operations -{ - public interface IOperationsProcessor - { - Task> ProcessAsync(List inputOps); - } - - public class OperationsProcessor : IOperationsProcessor - { - private readonly IOperationProcessorResolver _processorResolver; - private readonly DbContext _dbContext; - private readonly IJsonApiContext _jsonApiContext; - - public OperationsProcessor( - IOperationProcessorResolver processorResolver, - IDbContextResolver dbContextResolver, - IJsonApiContext jsonApiContext) - { - _processorResolver = processorResolver; - _dbContext = dbContextResolver.GetContext(); - _jsonApiContext = jsonApiContext; - } - - public async Task> ProcessAsync(List inputOps) - { - var outputOps = new List(); - var opIndex = 0; - OperationCode? lastAttemptedOperation = null; // used for error messages only - - using (var transaction = await _dbContext.Database.BeginTransactionAsync()) - { - try - { - foreach (var op in inputOps) - { - _jsonApiContext.BeginOperation(); - lastAttemptedOperation = op.Op; - await ProcessOperation(op, outputOps); - opIndex++; - } - - transaction.Commit(); - return outputOps; - } - catch (JsonApiException e) - { - transaction.Rollback(); - throw new JsonApiException(e.GetStatusCode(), $"Transaction failed on operation[{opIndex}] ({lastAttemptedOperation}).", e); - } - catch (Exception e) - { - transaction.Rollback(); - throw new JsonApiException(500, $"Transaction failed on operation[{opIndex}] ({lastAttemptedOperation}) for an unexpected reason.", e); - } - } - } - - private async Task ProcessOperation(Operation op, List outputOps) - { - ReplaceLocalIdsInResourceObject(op.DataObject, outputOps); - ReplaceLocalIdsInRef(op.Ref, outputOps); - - var processor = GetOperationsProcessor(op); - var resultOp = await processor.ProcessAsync(op); - - if (resultOp != null) - outputOps.Add(resultOp); - } - - private void ReplaceLocalIdsInResourceObject(ResourceObject resourceObject, List outputOps) - { - if (resourceObject == null) - return; - - // it is strange to me that a top level resource object might use a lid. - // by not replacing it, we avoid a case where the first operation is an 'add' with an 'lid' - // and we would be unable to locate the matching 'lid' in 'outputOps' - // - // we also create a scenario where I might try to update a resource I just created - // in this case, the 'data.id' will be null, but the 'ref.id' will be replaced by the correct 'id' from 'outputOps' - // - // if(HasLocalId(resourceObject)) - // resourceObject.Id = GetIdFromLocalId(outputOps, resourceObject.LocalId); - - if (resourceObject.Relationships != null) - { - foreach (var relationshipDictionary in resourceObject.Relationships) - { - if (relationshipDictionary.Value.IsHasMany) - { - foreach (var relationship in relationshipDictionary.Value.ManyData) - if (HasLocalId(relationship)) - relationship.Id = GetIdFromLocalId(outputOps, relationship.LocalId); - } - else - { - var relationship = relationshipDictionary.Value.SingleData; - if (HasLocalId(relationship)) - relationship.Id = GetIdFromLocalId(outputOps, relationship.LocalId); - } - } - } - } - - private void ReplaceLocalIdsInRef(ResourceReference resourceRef, List outputOps) - { - if (resourceRef == null) return; - if (HasLocalId(resourceRef)) - resourceRef.Id = GetIdFromLocalId(outputOps, resourceRef.LocalId); - } - - private bool HasLocalId(ResourceIdentifierObject rio) => string.IsNullOrEmpty(rio.LocalId) == false; - - private string GetIdFromLocalId(List outputOps, string localId) - { - var referencedOp = outputOps.FirstOrDefault(o => o.DataObject.LocalId == localId); - if (referencedOp == null) throw new JsonApiException(400, $"Could not locate lid '{localId}' in document."); - return referencedOp.DataObject.Id; - } - - private IOpProcessor GetOperationsProcessor(Operation op) - { - switch (op.Op) - { - case OperationCode.add: - return _processorResolver.LocateCreateService(op); - case OperationCode.get: - return _processorResolver.LocateGetService(op); - case OperationCode.remove: - return _processorResolver.LocateRemoveService(op); - case OperationCode.update: - return _processorResolver.LocateUpdateService(op); - default: - throw new JsonApiException(400, $"'{op.Op}' is not a valid operation code"); - } - } - } -} diff --git a/src/JsonApiDotNetCore/Services/Operations/Processors/CreateOpProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/Processors/CreateOpProcessor.cs deleted file mode 100644 index ebc600a5fb..0000000000 --- a/src/JsonApiDotNetCore/Services/Operations/Processors/CreateOpProcessor.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Threading.Tasks; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Operations; -using JsonApiDotNetCore.Serialization; - -namespace JsonApiDotNetCore.Services.Operations.Processors -{ - public interface ICreateOpProcessor : ICreateOpProcessor - where T : class, IIdentifiable - { } - - public interface ICreateOpProcessor : IOpProcessor - where T : class, IIdentifiable - { } - - public class CreateOpProcessor - : CreateOpProcessor, ICreateOpProcessor - where T : class, IIdentifiable - { - public CreateOpProcessor( - ICreateService service, - IJsonApiDeSerializer deSerializer, - IDocumentBuilder documentBuilder, - IResourceGraph resourceGraph - ) : base(service, deSerializer, documentBuilder, resourceGraph) - { } - } - - public class CreateOpProcessor : ICreateOpProcessor - where T : class, IIdentifiable - { - private readonly ICreateService _service; - private readonly IJsonApiDeSerializer _deSerializer; - private readonly IDocumentBuilder _documentBuilder; - private readonly IResourceGraph _resourceGraph; - - public CreateOpProcessor( - ICreateService service, - IJsonApiDeSerializer deSerializer, - IDocumentBuilder documentBuilder, - IResourceGraph resourceGraph) - { - _service = service; - _deSerializer = deSerializer; - _documentBuilder = documentBuilder; - _resourceGraph = resourceGraph; - } - - public async Task ProcessAsync(Operation operation) - { - var model = (T)_deSerializer.DocumentToObject(operation.DataObject); - var result = await _service.CreateAsync(model); - - var operationResult = new Operation - { - Op = OperationCode.add - }; - - operationResult.Data = _documentBuilder.GetData( - _resourceGraph.GetContextEntity(operation.GetResourceTypeName()), - result); - - // we need to persist the original request localId so that subsequent operations - // can locate the result of this operation by its localId - operationResult.DataObject.LocalId = operation.DataObject.LocalId; - - return operationResult; - } - } -} diff --git a/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs deleted file mode 100644 index 9415554bcb..0000000000 --- a/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Operations; -using JsonApiDotNetCore.Serialization; - -namespace JsonApiDotNetCore.Services.Operations.Processors -{ - /// - /// Handles all "" operations - /// - /// The resource type - public interface IGetOpProcessor : IGetOpProcessor - where T : class, IIdentifiable - { } - - /// - /// Handles all "" operations - /// - /// The resource type - /// The resource identifier type - public interface IGetOpProcessor : IOpProcessor - where T : class, IIdentifiable - { } - - /// - public class GetOpProcessor : GetOpProcessor, IGetOpProcessor - where T : class, IIdentifiable - { - /// - public GetOpProcessor( - IGetAllService getAll, - IGetByIdService getById, - IGetRelationshipService getRelationship, - IJsonApiDeSerializer deSerializer, - IDocumentBuilder documentBuilder, - IResourceGraph resourceGraph, - IJsonApiContext jsonApiContext - ) : base(getAll, getById, getRelationship, deSerializer, documentBuilder, resourceGraph, jsonApiContext) - { } - } - - /// - public class GetOpProcessor : IGetOpProcessor - where T : class, IIdentifiable - { - private readonly IGetAllService _getAll; - private readonly IGetByIdService _getById; - private readonly IGetRelationshipService _getRelationship; - private readonly IJsonApiDeSerializer _deSerializer; - private readonly IDocumentBuilder _documentBuilder; - private readonly IResourceGraph _resourceGraph; - private readonly IJsonApiContext _jsonApiContext; - - /// - public GetOpProcessor( - IGetAllService getAll, - IGetByIdService getById, - IGetRelationshipService getRelationship, - IJsonApiDeSerializer deSerializer, - IDocumentBuilder documentBuilder, - IResourceGraph resourceGraph, - IJsonApiContext jsonApiContext) - { - _getAll = getAll; - _getById = getById; - _getRelationship = getRelationship; - _deSerializer = deSerializer; - _documentBuilder = documentBuilder; - _resourceGraph = resourceGraph; - _jsonApiContext = jsonApiContext.ApplyContext(this); - } - - /// - public async Task ProcessAsync(Operation operation) - { - var operationResult = new Operation - { - Op = OperationCode.get - }; - - operationResult.Data = string.IsNullOrWhiteSpace(operation.Ref.Id) - ? await GetAllAsync(operation) - : string.IsNullOrWhiteSpace(operation.Ref.Relationship) - ? await GetByIdAsync(operation) - : await GetRelationshipAsync(operation); - - return operationResult; - } - - private async Task GetAllAsync(Operation operation) - { - var result = await _getAll.GetAsync(); - - var operations = new List(); - foreach (var resource in result) - { - var doc = _documentBuilder.GetData( - _resourceGraph.GetContextEntity(operation.GetResourceTypeName()), - resource); - operations.Add(doc); - } - - return operations; - } - - private async Task GetByIdAsync(Operation operation) - { - var id = GetReferenceId(operation); - var result = await _getById.GetAsync(id); - - // this is a bit ugly but we need to bomb the entire transaction if the entity cannot be found - // in the future it would probably be better to return a result status along with the doc to - // avoid throwing exceptions on 4xx errors. - // consider response type (status, document) - if (result == null) - throw new JsonApiException(404, $"Could not find '{operation.Ref.Type}' record with id '{operation.Ref.Id}'"); - - var doc = _documentBuilder.GetData( - _resourceGraph.GetContextEntity(operation.GetResourceTypeName()), - result); - - return doc; - } - - private async Task GetRelationshipAsync(Operation operation) - { - var id = GetReferenceId(operation); - var result = await _getRelationship.GetRelationshipAsync(id, operation.Ref.Relationship); - - // TODO: need a better way to get the ContextEntity from a relationship name - // when no generic parameter is available - var relationshipType = _resourceGraph.GetContextEntity(operation.GetResourceTypeName()) - .Relationships.Single(r => r.Is(operation.Ref.Relationship)).DependentType; - - var relatedContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationshipType); - - if (result == null) - return null; - - if (result is IIdentifiable singleResource) - return GetData(relatedContextEntity, singleResource); - - if (result is IEnumerable multipleResults) - return GetData(relatedContextEntity, multipleResults); - - throw new JsonApiException(500, - $"An unexpected type was returned from '{_getRelationship.GetType()}.{nameof(IGetRelationshipService.GetRelationshipAsync)}'.", - detail: $"Type '{result.GetType()} does not implement {nameof(IIdentifiable)} nor {nameof(IEnumerable)}'"); - } - - private ResourceObject GetData(ContextEntity contextEntity, IIdentifiable singleResource) - { - return _documentBuilder.GetData(contextEntity, singleResource); - } - - private List GetData(ContextEntity contextEntity, IEnumerable multipleResults) - { - var resources = new List(); - foreach (var singleResult in multipleResults) - { - if (singleResult is IIdentifiable resource) - resources.Add(_documentBuilder.GetData(contextEntity, resource)); - } - - return resources; - } - - private TId GetReferenceId(Operation operation) => TypeHelper.ConvertType(operation.Ref.Id); - } -} diff --git a/src/JsonApiDotNetCore/Services/Operations/Processors/RemoveOpProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/Processors/RemoveOpProcessor.cs deleted file mode 100644 index f3efe7939c..0000000000 --- a/src/JsonApiDotNetCore/Services/Operations/Processors/RemoveOpProcessor.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Threading.Tasks; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Operations; -using JsonApiDotNetCore.Serialization; - -namespace JsonApiDotNetCore.Services.Operations.Processors -{ - public interface IRemoveOpProcessor : IRemoveOpProcessor - where T : class, IIdentifiable - { } - - public interface IRemoveOpProcessor : IOpProcessor - where T : class, IIdentifiable - { } - - public class RemoveOpProcessor : RemoveOpProcessor, IRemoveOpProcessor - where T : class, IIdentifiable - { - public RemoveOpProcessor( - IDeleteService service, - IJsonApiDeSerializer deSerializer, - IDocumentBuilder documentBuilder, - IResourceGraph resourceGraph - ) : base(service, deSerializer, documentBuilder, resourceGraph) - { } - } - - public class RemoveOpProcessor : IRemoveOpProcessor - where T : class, IIdentifiable - { - private readonly IDeleteService _service; - private readonly IJsonApiDeSerializer _deSerializer; - private readonly IDocumentBuilder _documentBuilder; - private readonly IResourceGraph _resourceGraph; - - public RemoveOpProcessor( - IDeleteService service, - IJsonApiDeSerializer deSerializer, - IDocumentBuilder documentBuilder, - IResourceGraph resourceGraph) - { - _service = service; - _deSerializer = deSerializer; - _documentBuilder = documentBuilder; - _resourceGraph = resourceGraph; - } - - public async Task ProcessAsync(Operation operation) - { - var stringId = operation.Ref?.Id?.ToString(); - if (string.IsNullOrWhiteSpace(stringId)) - throw new JsonApiException(400, "The ref.id parameter is required for remove operations"); - - var id = TypeHelper.ConvertType(stringId); - var result = await _service.DeleteAsync(id); - - return null; - } - } -} diff --git a/src/JsonApiDotNetCore/Services/Operations/Processors/UpdateOpProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/Processors/UpdateOpProcessor.cs deleted file mode 100644 index 9b136b30c0..0000000000 --- a/src/JsonApiDotNetCore/Services/Operations/Processors/UpdateOpProcessor.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Threading.Tasks; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Operations; -using JsonApiDotNetCore.Serialization; - -namespace JsonApiDotNetCore.Services.Operations.Processors -{ - public interface IUpdateOpProcessor : IUpdateOpProcessor - where T : class, IIdentifiable - { } - - public interface IUpdateOpProcessor : IOpProcessor - where T : class, IIdentifiable - { } - - public class UpdateOpProcessor : UpdateOpProcessor, IUpdateOpProcessor - where T : class, IIdentifiable - { - public UpdateOpProcessor( - IUpdateService service, - IJsonApiDeSerializer deSerializer, - IDocumentBuilder documentBuilder, - IResourceGraph resourceGraph - ) : base(service, deSerializer, documentBuilder, resourceGraph) - { } - } - - public class UpdateOpProcessor : IUpdateOpProcessor - where T : class, IIdentifiable - { - private readonly IUpdateService _service; - private readonly IJsonApiDeSerializer _deSerializer; - private readonly IDocumentBuilder _documentBuilder; - private readonly IResourceGraph _resourceGraph; - - public UpdateOpProcessor( - IUpdateService service, - IJsonApiDeSerializer deSerializer, - IDocumentBuilder documentBuilder, - IResourceGraph resourceGraph) - { - _service = service; - _deSerializer = deSerializer; - _documentBuilder = documentBuilder; - _resourceGraph = resourceGraph; - } - - public async Task ProcessAsync(Operation operation) - { - if (string.IsNullOrWhiteSpace(operation?.DataObject?.Id?.ToString())) - throw new JsonApiException(400, "The data.id parameter is required for replace operations"); - - var model = (T)_deSerializer.DocumentToObject(operation.DataObject); - - var result = await _service.UpdateAsync(model.Id, model); - if (result == null) - throw new JsonApiException(404, $"Could not find an instance of '{operation.DataObject.Type}' with id {operation.DataObject.Id}"); - - var operationResult = new Operation - { - Op = OperationCode.update - }; - - operationResult.Data = _documentBuilder.GetData(_resourceGraph.GetContextEntity(operation.GetResourceTypeName()), result); - - return operationResult; - } - } -} diff --git a/src/JsonApiDotNetCore/Services/QueryAccessor.cs b/src/JsonApiDotNetCore/Services/QueryAccessor.cs deleted file mode 100644 index dc9ff7ef0a..0000000000 --- a/src/JsonApiDotNetCore/Services/QueryAccessor.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Linq; -using JsonApiDotNetCore.Internal; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCore.Services -{ - public interface IQueryAccessor - { - bool TryGetValue(string key, out T value); - - /// - /// Gets the query value and throws a if it is not present. - /// If the exception is not caught, the middleware will return an HTTP 422 response. - /// - /// - T GetRequired(string key); - } - - public class QueryAccessor : IQueryAccessor - { - private readonly IJsonApiContext _jsonApiContext; - private readonly ILogger _logger; - - public QueryAccessor( - IJsonApiContext jsonApiContext, - ILogger logger) - { - _jsonApiContext = jsonApiContext; - _logger = logger; - } - - public T GetRequired(string key) - { - if (TryGetValue(key, out T result) == false) - throw new JsonApiException(422, $"'{key}' is not a valid '{typeof(T).Name}' value for query parameter {key}"); - - return result; - } - - public bool TryGetValue(string key, out T value) - { - value = default(T); - - var stringValue = GetFilterValue(key); - if (stringValue == null) - { - if (_logger.IsEnabled(LogLevel.Information)) - { - _logger.LogInformation($"'{key}' was not found in the query collection"); - } - - return false; - } - - try - { - value = TypeHelper.ConvertType(stringValue); - return true; - } - catch (FormatException) - { - if (_logger.IsEnabled(LogLevel.Information)) - { - _logger.LogInformation( - $"'{value}' is not a valid '{typeof(T).Name}' value for query parameter {key}"); - } - - return false; - } - } - - private string GetFilterValue(string key) { - var publicValue = _jsonApiContext.QuerySet.Filters - .FirstOrDefault(f => string.Equals(f.Attribute, key, StringComparison.OrdinalIgnoreCase))?.Value; - - if(publicValue != null) - return publicValue; - - var internalValue = _jsonApiContext.QuerySet.Filters - .FirstOrDefault(f => string.Equals(f.Attribute, key, StringComparison.OrdinalIgnoreCase))?.Value; - - if(internalValue != null) { - _logger.LogWarning("Locating filters by the internal propterty name is deprecated. You should use the public attribute name instead."); - return publicValue; - } - - return null; - } - } -} diff --git a/src/JsonApiDotNetCore/Services/QueryComposer.cs b/src/JsonApiDotNetCore/Services/QueryComposer.cs deleted file mode 100644 index e365811704..0000000000 --- a/src/JsonApiDotNetCore/Services/QueryComposer.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Internal.Query; - -namespace JsonApiDotNetCore.Services -{ - public interface IQueryComposer - { - string Compose(IJsonApiContext jsonApiContext); - } - - public class QueryComposer : IQueryComposer - { - public string Compose(IJsonApiContext jsonApiContext) - { - string result = ""; - if (jsonApiContext != null && jsonApiContext.QuerySet != null) - { - List filterQueries = jsonApiContext.QuerySet.Filters; - if (filterQueries.Count > 0) - { - foreach (FilterQuery filter in filterQueries) - { - result += ComposeSingleFilter(filter); - } - } - } - return result; - } - - private string ComposeSingleFilter(FilterQuery query) - { - var result = "&filter"; - var operation = string.IsNullOrWhiteSpace(query.Operation) ? query.Operation : query.Operation + ":"; - result += QueryConstants.OPEN_BRACKET + query.Attribute + QueryConstants.CLOSE_BRACKET + "=" + operation + query.Value; - return result; - } - } -} diff --git a/src/JsonApiDotNetCore/Services/QueryParser.cs b/src/JsonApiDotNetCore/Services/QueryParser.cs deleted file mode 100644 index bffcdc9a59..0000000000 --- a/src/JsonApiDotNetCore/Services/QueryParser.cs +++ /dev/null @@ -1,251 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Query; -using JsonApiDotNetCore.Models; -using Microsoft.AspNetCore.Http; - -namespace JsonApiDotNetCore.Services -{ - public interface IQueryParser - { - QuerySet Parse(IQueryCollection query); - } - - public class QueryParser : IQueryParser - { - private readonly IControllerContext _controllerContext; - private readonly JsonApiOptions _options; - - public QueryParser( - IControllerContext controllerContext, - JsonApiOptions options) - { - _controllerContext = controllerContext; - _options = options; - } - - public virtual QuerySet Parse(IQueryCollection query) - { - var querySet = new QuerySet(); - var disabledQueries = _controllerContext.GetControllerAttribute()?.QueryParams ?? QueryParams.None; - - foreach (var pair in query) - { - if (pair.Key.StartsWith(QueryConstants.FILTER)) - { - if (disabledQueries.HasFlag(QueryParams.Filter) == false) - querySet.Filters.AddRange(ParseFilterQuery(pair.Key, pair.Value)); - continue; - } - - if (pair.Key.StartsWith(QueryConstants.SORT)) - { - if (disabledQueries.HasFlag(QueryParams.Sort) == false) - querySet.SortParameters = ParseSortParameters(pair.Value); - continue; - } - - if (pair.Key.StartsWith(QueryConstants.INCLUDE)) - { - if (disabledQueries.HasFlag(QueryParams.Include) == false) - querySet.IncludedRelationships = ParseIncludedRelationships(pair.Value); - continue; - } - - if (pair.Key.StartsWith(QueryConstants.PAGE)) - { - if (disabledQueries.HasFlag(QueryParams.Page) == false) - querySet.PageQuery = ParsePageQuery(querySet.PageQuery, pair.Key, pair.Value); - continue; - } - - if (pair.Key.StartsWith(QueryConstants.FIELDS)) - { - if (disabledQueries.HasFlag(QueryParams.Fields) == false) - querySet.Fields = ParseFieldsQuery(pair.Key, pair.Value); - continue; - } - - if (_options.AllowCustomQueryParameters == false) - throw new JsonApiException(400, $"{pair} is not a valid query."); - } - - return querySet; - } - - protected virtual List ParseFilterQuery(string key, string value) - { - // expected input = filter[id]=1 - // expected input = filter[id]=eq:1 - var queries = new List(); - var propertyName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; - - // InArray case - string op = GetFilterOperation(value); - if (string.Equals(op, FilterOperations.@in.ToString(), StringComparison.OrdinalIgnoreCase) - || string.Equals(op, FilterOperations.nin.ToString(), StringComparison.OrdinalIgnoreCase)) - { - (var operation, var filterValue) = ParseFilterOperation(value); - queries.Add(new FilterQuery(propertyName, filterValue, op)); - } - else - { - var values = value.Split(QueryConstants.COMMA); - foreach (var val in values) - { - (var operation, var filterValue) = ParseFilterOperation(val); - queries.Add(new FilterQuery(propertyName, filterValue, operation)); - } - } - - return queries; - } - - protected virtual (string operation, string value) ParseFilterOperation(string value) - { - if (value.Length < 3) - return (string.Empty, value); - - var operation = GetFilterOperation(value); - var values = value.Split(QueryConstants.COLON); - - if (string.IsNullOrEmpty(operation)) - return (string.Empty, value); - - value = string.Join(QueryConstants.COLON_STR, values.Skip(1)); - - return (operation, value); - } - - protected virtual PageQuery ParsePageQuery(PageQuery pageQuery, string key, string value) - { - // expected input = page[size]=10 - // page[number]=1 - pageQuery = pageQuery ?? new PageQuery(); - - var propertyName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; - - const string SIZE = "size"; - const string NUMBER = "number"; - - if (propertyName == SIZE) - pageQuery.PageSize = int.TryParse(value, out var pageSize) ? - pageSize : - throw new JsonApiException(400, $"Invalid page size '{value}'"); - - else if (propertyName == NUMBER) - pageQuery.PageOffset = int.TryParse(value, out var pageOffset) ? - pageOffset : - throw new JsonApiException(400, $"Invalid page size '{value}'"); - - return pageQuery; - } - - // sort=id,name - // sort=-id - protected virtual List ParseSortParameters(string value) - { - var sortParameters = new List(); - - const char DESCENDING_SORT_OPERATOR = '-'; - var sortSegments = value.Split(QueryConstants.COMMA); - if(sortSegments.Where(s => s == string.Empty).Count() >0) - { - throw new JsonApiException(400, "The sort URI segment contained a null value."); - } - foreach (var sortSegment in sortSegments) - { - var propertyName = sortSegment; - var direction = SortDirection.Ascending; - - if (sortSegment[0] == DESCENDING_SORT_OPERATOR) - { - direction = SortDirection.Descending; - propertyName = propertyName.Substring(1); - } - - sortParameters.Add(new SortQuery(direction, propertyName)); - }; - - return sortParameters; - } - - protected virtual List ParseIncludedRelationships(string value) - { - return value - .Split(QueryConstants.COMMA) - .ToList(); - } - - protected virtual List ParseFieldsQuery(string key, string value) - { - // expected: fields[TYPE]=prop1,prop2 - var typeName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; - var includedFields = new List { nameof(Identifiable.Id) }; - - var relationship = _controllerContext.RequestEntity.Relationships.SingleOrDefault(a => a.Is(typeName)); - if (relationship == default && string.Equals(typeName, _controllerContext.RequestEntity.EntityName, StringComparison.OrdinalIgnoreCase) == false) - return includedFields; - - var fields = value.Split(QueryConstants.COMMA); - foreach (var field in fields) - { - if (relationship != default) - { - var relationProperty = _options.ResourceGraph.GetContextEntity(relationship.DependentType); - var attr = relationProperty.Attributes.SingleOrDefault(a => a.Is(field)); - if(attr == null) - throw new JsonApiException(400, $"'{relationship.DependentType.Name}' does not contain '{field}'."); - - // e.g. "Owner.Name" - includedFields.Add(relationship.InternalRelationshipName + "." + attr.InternalAttributeName); - } - else - { - var attr = _controllerContext.RequestEntity.Attributes.SingleOrDefault(a => a.Is(field)); - if (attr == null) - throw new JsonApiException(400, $"'{_controllerContext.RequestEntity.EntityName}' does not contain '{field}'."); - - // e.g. "Name" - includedFields.Add(attr.InternalAttributeName); - } - } - - return includedFields; - } - - protected virtual AttrAttribute GetAttribute(string propertyName) - { - try - { - return _controllerContext - .RequestEntity - .Attributes - .Single(attr => attr.Is(propertyName)); - } - catch (InvalidOperationException e) - { - throw new JsonApiException(400, $"Attribute '{propertyName}' does not exist on resource '{_controllerContext.RequestEntity.EntityName}'", e); - } - } - - private string GetFilterOperation(string value) - { - var values = value.Split(QueryConstants.COLON); - - if (values.Length == 1) - return string.Empty; - - var operation = values[0]; - // remove prefix from value - if (Enum.TryParse(operation, out FilterOperations op) == false) - return string.Empty; - - return operation; - } - } -} diff --git a/src/JsonApiDotNetCore/Services/ResourceDefinitionProvider.cs b/src/JsonApiDotNetCore/Services/ResourceDefinitionProvider.cs new file mode 100644 index 0000000000..5a9e4b655c --- /dev/null +++ b/src/JsonApiDotNetCore/Services/ResourceDefinitionProvider.cs @@ -0,0 +1,26 @@ +using System; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Query +{ + /// + internal class ResourceDefinitionProvider : IResourceDefinitionProvider + { + private readonly IResourceGraph _resourceContextProvider; + private readonly IScopedServiceProvider _serviceProvider; + + public ResourceDefinitionProvider(IResourceGraph resourceContextProvider, IScopedServiceProvider serviceProvider) + { + _resourceContextProvider = resourceContextProvider; + _serviceProvider = serviceProvider; + } + + /// + public IResourceDefinition Get(Type resourceType) + { + return (IResourceDefinition)_serviceProvider.GetService(_resourceContextProvider.GetResourceContext(resourceType).ResourceDefinitionType); + } + } +} diff --git a/test/DiscoveryTests/DiscoveryTests.csproj b/test/DiscoveryTests/DiscoveryTests.csproj index eeb13485f3..d4458df6dd 100644 --- a/test/DiscoveryTests/DiscoveryTests.csproj +++ b/test/DiscoveryTests/DiscoveryTests.csproj @@ -15,5 +15,4 @@ - - + \ No newline at end of file diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 09ffcfcfc1..cf87a3f537 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -1,97 +1,124 @@ -using GettingStarted.Models; -using GettingStarted.ResourceDefinitionExample; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Graph; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; +using System.Collections.Generic; +using GettingStarted.Models; +using GettingStarted.ResourceDefinitionExample; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Graph; +using JsonApiDotNetCore.Hooks; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Internal.Generics; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Query; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Server.Builders; +using JsonApiDotNetCore.Services; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using Xunit; - -namespace DiscoveryTests -{ - public class ServiceDiscoveryFacadeTests - { - private readonly IServiceCollection _services = new ServiceCollection(); - private readonly ResourceGraphBuilder _graphBuilder = new ResourceGraphBuilder(); +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace DiscoveryTests +{ + public class ServiceDiscoveryFacadeTests + { + private readonly IServiceCollection _services = new ServiceCollection(); + private readonly ResourceGraphBuilder _resourceGraphBuilder = new ResourceGraphBuilder(); public ServiceDiscoveryFacadeTests() { - var contextMock = new Mock(); - var dbResolverMock = new Mock(); - dbResolverMock.Setup(m => m.GetContext()).Returns(new Mock().Object); + var contextMock = new Mock(); + var dbResolverMock = new Mock(); + dbResolverMock.Setup(m => m.GetContext()).Returns(new Mock().Object); TestModelRepository._dbContextResolver = dbResolverMock.Object; + _services.AddSingleton(new JsonApiOptions()); + _services.AddScoped((_) => new Mock().Object); + _services.AddScoped((_) => new Mock().Object); + _services.AddScoped((_) => new Mock().Object); + _services.AddScoped((_) => new Mock().Object); + _services.AddScoped((_) => new Mock().Object); + _services.AddScoped((_) => new Mock().Object); + } + + private ServiceDiscoveryFacade _facade => new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); + + [Fact] + public void AddAssembly_Adds_All_Resources_To_Graph() + { + // Arrange, act + _facade.AddAssembly(typeof(Person).Assembly); + + // Assert + var resourceGraph = _resourceGraphBuilder.Build(); + var personResource = resourceGraph.GetResourceContext(typeof(Person)); + var articleResource = resourceGraph.GetResourceContext(typeof(Article)); + var modelResource = resourceGraph.GetResourceContext(typeof(Model)); + + Assert.NotNull(personResource); + Assert.NotNull(articleResource); + Assert.NotNull(modelResource); } - private ServiceDiscoveryFacade _facade => new ServiceDiscoveryFacade(_services, _graphBuilder); - - [Fact] - public void AddAssembly_Adds_All_Resources_To_Graph() - { - // arrange, act - _facade.AddAssembly(typeof(Person).Assembly); - - // assert - var graph = _graphBuilder.Build(); - var personResource = graph.GetContextEntity(typeof(Person)); - var articleResource = graph.GetContextEntity(typeof(Article)); - var modelResource = graph.GetContextEntity(typeof(Model)); - - Assert.NotNull(personResource); - Assert.NotNull(articleResource); - Assert.NotNull(modelResource); - } - - [Fact] - public void AddCurrentAssembly_Adds_Resources_To_Graph() - { - // arrange, act - _facade.AddCurrentAssembly(); - - // assert - var graph = _graphBuilder.Build(); - var testModelResource = graph.GetContextEntity(typeof(TestModel)); - Assert.NotNull(testModelResource); - } - - [Fact] - public void AddCurrentAssembly_Adds_Services_To_Container() - { - // arrange, act - _facade.AddCurrentAssembly(); - - // assert - var services = _services.BuildServiceProvider(); - Assert.IsType(services.GetService>()); - } - - [Fact] - public void AddCurrentAssembly_Adds_Repositories_To_Container() - { - // arrange, act - _facade.AddCurrentAssembly(); - - // assert - var services = _services.BuildServiceProvider(); - Assert.IsType(services.GetService>()); - } - - public class TestModel : Identifiable { } - - public class TestModelService : EntityResourceService - { - private static IEntityRepository _repo = new Mock>().Object; - private static IJsonApiContext _jsonApiContext = new Mock().Object; - public TestModelService() : base(_jsonApiContext, _repo) { } - } - - public class TestModelRepository : DefaultEntityRepository - { - internal static IDbContextResolver _dbContextResolver; - private static IJsonApiContext _jsonApiContext = new Mock().Object; - public TestModelRepository() : base(_jsonApiContext, _dbContextResolver) { } - } - } -} + [Fact] + public void AddCurrentAssembly_Adds_Resources_To_Graph() + { + // Arrange, act + _facade.AddCurrentAssembly(); + + // Assert + var resourceGraph = _resourceGraphBuilder.Build(); + var testModelResource = resourceGraph.GetResourceContext(typeof(TestModel)); + Assert.NotNull(testModelResource); + } + + [Fact] + public void AddCurrentAssembly_Adds_Services_To_Container() + { + // Arrange, act + _facade.AddCurrentAssembly(); + + // Assert + var services = _services.BuildServiceProvider(); + var service = services.GetService>(); + Assert.IsType(service); + } + + [Fact] + public void AddCurrentAssembly_Adds_Repositories_To_Container() + { + // Arrange, act + _facade.AddCurrentAssembly(); + + // Assert + var services = _services.BuildServiceProvider(); + Assert.IsType(services.GetService>()); + } + + public class TestModel : Identifiable { } + + public class TestModelService : DefaultResourceService + { + private static IResourceRepository _repo = new Mock>().Object; + + public TestModelService(IEnumerable queryParameters, + IJsonApiOptions options, + IResourceRepository repository, + IResourceContextProvider provider, + IResourceHookExecutor hookExecutor = null, + ILoggerFactory loggerFactory = null) + : base(queryParameters, options, repository, provider, hookExecutor, loggerFactory) { } + } + + public class TestModelRepository : DefaultResourceRepository + { + internal static IDbContextResolver _dbContextResolver; + + public TestModelRepository(ITargetedFields targetedFields, + IResourceGraph resourceGraph, + IGenericServiceFactory genericServiceFactory) + : base(targetedFields, _dbContextResolver, resourceGraph, genericServiceFactory) { } + } + } +} diff --git a/test/IntegrationTests/Data/EntityRepositoryTests.cs b/test/IntegrationTests/Data/EntityRepositoryTests.cs new file mode 100644 index 0000000000..e0699ceb2b --- /dev/null +++ b/test/IntegrationTests/Data/EntityRepositoryTests.cs @@ -0,0 +1,195 @@ +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using Microsoft.EntityFrameworkCore; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Xunit; + + +namespace JADNC.IntegrationTests.Data +{ + public class EntityRepositoryTests + { + + + public EntityRepositoryTests() + { + } + + [Fact] + public async Task UpdateAsync_AttributesUpdated_ShouldHaveSpecificallyThoseAttributesUpdated() + { + // Arrange + var itemId = 213; + var seed = Guid.NewGuid(); + using (var arrangeContext = GetContext(seed)) + { + var (repository, targetedFields) = Setup(arrangeContext); + var todoItemUpdates = new TodoItem + { + Id = itemId, + Description = Guid.NewGuid().ToString() + }; + arrangeContext.Add(todoItemUpdates); + arrangeContext.SaveChanges(); + + var descAttr = new AttrAttribute("description", "Description") + { + PropertyInfo = typeof(TodoItem).GetProperty(nameof(TodoItem.Description)) + }; + targetedFields.Setup(m => m.Attributes).Returns(new List { descAttr }); + targetedFields.Setup(m => m.Relationships).Returns(new List()); + + // Act + var updatedItem = await repository.UpdateAsync(todoItemUpdates); + } + + // Assert - in different context + using var assertContext = GetContext(seed); + { + var (repository, targetedFields) = Setup(assertContext); + + var fetchedTodo = repository.Get(itemId).First(); + Assert.NotNull(fetchedTodo); + Assert.Equal(fetchedTodo.Ordinal, fetchedTodo.Ordinal); + Assert.Equal(fetchedTodo.Description, fetchedTodo.Description); + + } + } + + [Theory] + [InlineData(3, 2, new[] { 4, 5, 6 })] + [InlineData(8, 2, new[] { 9 })] + [InlineData(20, 1, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 })] + public async Task Paging_PageNumberIsPositive_ReturnCorrectIdsAtTheFront(int pageSize, int pageNumber, int[] expectedResult) + { + // Arrange + using var context = GetContext(); + var (repository, targetedFields) = Setup(context); + context.AddRange(TodoItems(1, 2, 3, 4, 5, 6, 7, 8, 9)); + await context.SaveChangesAsync(); + + // Act + var result = await repository.PageAsync(context.Set(), pageSize, pageNumber); + + // Assert + Assert.Equal(TodoItems(expectedResult), result, new IdComparer()); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(-10)] + public async Task Paging_PageSizeNonPositive_DoNothing(int pageSize) + { + // Arrange + using var context = GetContext(); + var (repository, targetedFields) = Setup(context); + var items = TodoItems(2, 3, 1); + context.AddRange(items); + await context.SaveChangesAsync(); + + // Act + var result = await repository.PageAsync(context.Set(), pageSize, 3); + + // Assert + Assert.Equal(items.ToList(), result.ToList(), new IdComparer()); + } + + [Fact] + public async Task Paging_PageNumberDoesNotExist_ReturnEmptyAQueryable() + { + // Arrange + var items = TodoItems(2, 3, 1); + using var context = GetContext(); + var (repository, targetedFields) = Setup(context); + context.AddRange(items); + + // Act + var result = await repository.PageAsync(context.Set(), 2, 3); + + // Assert + Assert.Empty(result); + } + + [Fact] + public async Task Paging_PageNumberIsZero_PretendsItsOne() + { + // Arrange + using var context = GetContext(); + var (repository, targetedFields) = Setup(context); + context.AddRange(TodoItems(2, 3, 4, 5, 6, 7, 8, 9)); + await context.SaveChangesAsync(); + + // Act + var result = await repository.PageAsync(entities: context.Set(), pageSize: 1, pageNumber: 0); + + // Assert + Assert.Equal(TodoItems(2), result, new IdComparer()); + } + + [Theory] + [InlineData(6, -1, new[] { 4, 5, 6, 7, 8, 9 })] + [InlineData(6, -2, new[] { 1, 2, 3 })] + [InlineData(20, -1, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 })] + public async Task Paging_PageNumberIsNegative_GiveBackReverseAmountOfIds(int pageSize, int pageNumber, int[] expectedIds) + { + // Arrange + using var context = GetContext(); + var repository = Setup(context).Repository; + context.AddRange(TodoItems(1, 2, 3, 4, 5, 6, 7, 8, 9)); + context.SaveChanges(); + + // Act + var result = await repository.PageAsync(context.Set(), pageSize, pageNumber); + + // Assert + Assert.Equal(TodoItems(expectedIds), result, new IdComparer()); + } + + + private (DefaultResourceRepository Repository, Mock TargetedFields) Setup(AppDbContext context) + { + var contextResolverMock = new Mock(); + contextResolverMock.Setup(m => m.GetContext()).Returns(context); + var resourceGraph = new ResourceGraphBuilder().AddResource().Build(); + var targetedFields = new Mock(); + var repository = new DefaultResourceRepository(targetedFields.Object, contextResolverMock.Object, resourceGraph, null); + return (repository, targetedFields); + } + + private AppDbContext GetContext(Guid? seed = null) + { + Guid actualSeed = seed == null ? Guid.NewGuid() : seed.GetValueOrDefault(); + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: $"IntegrationDatabaseRepository{actualSeed}") + .Options; + var context = new AppDbContext(options); + + context.TodoItems.RemoveRange(context.TodoItems.ToList()); + return context; + } + + private static TodoItem[] TodoItems(params int[] ids) + { + return ids.Select(id => new TodoItem { Id = id }).ToArray(); + } + + private class IdComparer : IEqualityComparer + where T : IIdentifiable + { + public bool Equals(T x, T y) => x?.StringId == y?.StringId; + + public int GetHashCode(T obj) => obj?.StringId?.GetHashCode() ?? 0; + } + } +} diff --git a/test/IntegrationTests/IntegrationTests.csproj b/test/IntegrationTests/IntegrationTests.csproj new file mode 100644 index 0000000000..23141edea5 --- /dev/null +++ b/test/IntegrationTests/IntegrationTests.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp3.0 + + false + + JADNC.IntegrationTests + + + + + + + + + + + + + + + + + + + diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/CamelCasedModelsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/CamelCasedModelsControllerTests.cs index b2059c10c9..a9b827ebc5 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/CamelCasedModelsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/CamelCasedModelsControllerTests.cs @@ -4,8 +4,6 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using Bogus; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -19,16 +17,14 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance [Collection("WebHostCollection")] public class CamelCasedModelsControllerTests { - private TestFixture _fixture; + private TestFixture _fixture; private AppDbContext _context; - private IJsonApiContext _jsonApiContext; private Faker _faker; - public CamelCasedModelsControllerTests(TestFixture fixture) + public CamelCasedModelsControllerTests(TestFixture fixture) { _fixture = fixture; _context = fixture.GetService(); - _jsonApiContext = fixture.GetService(); _faker = new Faker() .RuleFor(m => m.CompoundAttr, f => f.Lorem.Sentence()); } @@ -42,7 +38,7 @@ public async Task Can_Get_CamelCasedModels() _context.SaveChanges(); var httpMethod = new HttpMethod("GET"); - var route = "/camelCasedModels"; + var route = "api/v1/camelCasedModels"; var builder = new WebHostBuilder() .UseStartup(); var server = new TestServer(builder); @@ -52,8 +48,7 @@ public async Task Can_Get_CamelCasedModels() // Act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService() - .DeserializeList(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeList(body).Data; // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -70,18 +65,19 @@ public async Task Can_Get_CamelCasedModels_ById() _context.SaveChanges(); var httpMethod = new HttpMethod("GET"); - var route = $"/camelCasedModels/{model.Id}"; + var route = $"api/v1/camelCasedModels/{model.Id}"; + var request = new HttpRequestMessage(httpMethod, route); + + // unnecessary, will fix in 4.1 var builder = new WebHostBuilder() - .UseStartup(); + .UseStartup(); var server = new TestServer(builder); var client = server.CreateClient(); - var request = new HttpRequestMessage(httpMethod, route); // Act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (CamelCasedModel)_fixture.GetService() - .Deserialize(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeSingle(body).Data; // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -106,7 +102,7 @@ public async Task Can_Post_CamelCasedModels() } }; var httpMethod = new HttpMethod("POST"); - var route = $"/camelCasedModels"; + var route = $"api/v1/camelCasedModels"; var builder = new WebHostBuilder() .UseStartup(); var server = new TestServer(builder); @@ -124,13 +120,12 @@ public async Task Can_Post_CamelCasedModels() Assert.NotNull(body); Assert.NotEmpty(body); - var deserializedBody = (CamelCasedModel)_fixture.GetService() - .Deserialize(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeSingle(body).Data; Assert.Equal(model.CompoundAttr, deserializedBody.CompoundAttr); } [Fact] - public async Task Can_Patch_CamelCasedModels() + public async Task RoutingPatch_RouteIsCamelcased_ResponseOKAndCompoundAttrIsAvailable() { // Arrange var model = _faker.Generate(); @@ -150,11 +145,11 @@ public async Task Can_Patch_CamelCasedModels() } } }; - var httpMethod = new HttpMethod("PATCH"); - var route = $"/camelCasedModels/{model.Id}"; - var builder = new WebHostBuilder() - .UseStartup(); - var server = new TestServer(builder); + var httpMethod = HttpMethod.Patch; + var route = $"api/v1/camelCasedModels/{model.Id}"; + var builder = new WebHostBuilder().UseStartup(); + + using var server = new TestServer(builder); var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); request.Content = new StringContent(JsonConvert.SerializeObject(content)); @@ -169,8 +164,7 @@ public async Task Can_Patch_CamelCasedModels() Assert.NotNull(body); Assert.NotEmpty(body); - var deserializedBody = (CamelCasedModel)_fixture.GetService() - .Deserialize(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeSingle(body).Data; Assert.Equal(newModel.CompoundAttr, deserializedBody.CompoundAttr); } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs index 478f40f14f..4a23da42ac 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs @@ -17,11 +17,11 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility [Collection("WebHostCollection")] public class CustomControllerTests { - private TestFixture _fixture; + private TestFixture _fixture; private Faker _todoItemFaker; private Faker _personFaker; - public CustomControllerTests(TestFixture fixture) + public CustomControllerTests(TestFixture fixture) { _fixture = fixture; _todoItemFaker = new Faker() @@ -35,7 +35,7 @@ public CustomControllerTests(TestFixture fixture) [Fact] public async Task NonJsonApiControllers_DoNotUse_Dasherized_Routes() { - // arrange + // Arrange var builder = new WebHostBuilder() .UseStartup(); var httpMethod = new HttpMethod("GET"); @@ -45,10 +45,10 @@ public async Task NonJsonApiControllers_DoNotUse_Dasherized_Routes() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); - - // assert + + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); } @@ -65,10 +65,10 @@ public async Task CustomRouteControllers_Uses_Dasherized_Collection_Route() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); } @@ -92,10 +92,10 @@ public async Task CustomRouteControllers_Uses_Dasherized_Item_Route() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); } @@ -110,8 +110,7 @@ public async Task CustomRouteControllers_Creates_Proper_Relationship_Links() context.TodoItems.Add(todoItem); await context.SaveChangesAsync(); - var builder = new WebHostBuilder() - .UseStartup(); + var builder = new WebHostBuilder().UseStartup(); var httpMethod = new HttpMethod("GET"); var route = $"/custom/route/todo-items/{todoItem.Id}"; @@ -119,14 +118,16 @@ public async Task CustomRouteControllers_Creates_Proper_Relationship_Links() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act & assert + // Act var response = await client.SendAsync(request); + + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); var deserializedBody = JsonConvert.DeserializeObject(body); - var result = deserializedBody["data"]["relationships"]["owner"]["links"]["related"].ToString(); + var result = deserializedBody["data"]["relationships"]["owner"]["links"]["related"].ToString(); Assert.EndsWith($"{route}/owner", deserializedBody["data"]["relationships"]["owner"]["links"]["related"].ToString()); } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs deleted file mode 100644 index fcc6e5ffde..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Newtonsoft.Json; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Serialization; -using Xunit; - -namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility -{ - public class CustomErrorTests - { - [Fact] - public void Can_Return_Custom_Error_Types() - { - // arrange - var error = new CustomError(507, "title", "detail", "custom"); - var errorCollection = new ErrorCollection(); - errorCollection.Add(error); - - var expectedJson = JsonConvert.SerializeObject(new { - errors = new dynamic[] { - new { - myCustomProperty = "custom", - title = "title", - detail = "detail", - status = "507" - } - } - }); - - // act - var result = new JsonApiSerializer(null, null, null) - .Serialize(errorCollection); - - // assert - Assert.Equal(expectedJson, result); - - } - - class CustomError : Error { - public CustomError(int status, string title, string detail, string myProp) - : base(status, title, detail) - { - MyCustomProperty = myProp; - } - public string MyCustomProperty { get; set; } - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs index 2030694918..9081dac41e 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Models; +using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Newtonsoft.Json; @@ -13,11 +14,11 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility [Collection("WebHostCollection")] public class NullValuedAttributeHandlingTests : IAsyncLifetime { - private readonly TestFixture _fixture; + private readonly TestFixture _fixture; private readonly AppDbContext _dbContext; private readonly TodoItem _todoItem; - public NullValuedAttributeHandlingTests(TestFixture fixture) + public NullValuedAttributeHandlingTests(TestFixture fixture) { _fixture = fixture; _dbContext = fixture.GetService(); @@ -67,39 +68,32 @@ public async Task CheckNullBehaviorCombination(bool? omitNullValuedAttributes, b // Override some null handling options NullAttributeResponseBehavior nullAttributeResponseBehavior; if (omitNullValuedAttributes.HasValue && allowClientOverride.HasValue) - { nullAttributeResponseBehavior = new NullAttributeResponseBehavior(omitNullValuedAttributes.Value, allowClientOverride.Value); - } else if (omitNullValuedAttributes.HasValue) - { nullAttributeResponseBehavior = new NullAttributeResponseBehavior(omitNullValuedAttributes.Value); - } else if (allowClientOverride.HasValue) - { nullAttributeResponseBehavior = new NullAttributeResponseBehavior(allowClientOverride: allowClientOverride.Value); - } else - { nullAttributeResponseBehavior = new NullAttributeResponseBehavior(); - } - var jsonApiOptions = _fixture.GetService(); + + var jsonApiOptions = _fixture.GetService(); jsonApiOptions.NullAttributeResponseBehavior = nullAttributeResponseBehavior; jsonApiOptions.AllowCustomQueryParameters = true; var httpMethod = new HttpMethod("GET"); var queryString = allowClientOverride.HasValue - ? $"&omitNullValuedAttributes={clientOverride}" + ? $"&omitNull={clientOverride}" : ""; - var route = $"/api/v1/todo-items/{_todoItem.Id}?include=owner{queryString}"; + var route = $"/api/v1/todo-items/{_todoItem.Id}?include=owner{queryString}"; var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); var deserializeBody = JsonConvert.DeserializeObject(body); - // assert. does response contain a null valued attribute - Assert.Equal(omitsNulls, !deserializeBody.Data.Attributes.ContainsKey("description")); + // Assert: does response contain a null valued attribute? + Assert.Equal(omitsNulls, !deserializeBody.SingleData.Attributes.ContainsKey("description")); Assert.Equal(omitsNulls, !deserializeBody.Included[0].Attributes.ContainsKey("last-name")); } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RepositoryOverrideTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RepositoryOverrideTests.cs deleted file mode 100644 index d63575e263..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RepositoryOverrideTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExample.Data; -using JsonApiDotNetCoreExample.Models; -using JsonApiDotNetCoreExampleTests.Services; -using JsonApiDotNetCoreExampleTests.Startups; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Xunit; - -namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility -{ - [Collection("WebHostCollection")] - public class RepositoryOverrideTests - { - private TestFixture _fixture; - - public RepositoryOverrideTests(TestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task Total_Record_Count_Included() - { - // arrange - var builder = new WebHostBuilder() - .UseStartup(); - var server = new TestServer(builder); - var client = server.CreateClient(); - var context = (AppDbContext)server.Host.Services.GetService(typeof(AppDbContext)); - var jsonApiContext = (IJsonApiContext)server.Host.Services.GetService(typeof(IJsonApiContext)); - - var person = new Person(); - context.People.Add(person); - var ownedTodoItem = new TodoItem(); - var unOwnedTodoItem = new TodoItem(); - ownedTodoItem.Owner = person; - context.TodoItems.Add(ownedTodoItem); - context.TodoItems.Add(unOwnedTodoItem); - context.SaveChanges(); - - var authService = (IAuthorizationService)server.Host.Services.GetService(typeof(IAuthorizationService)); - authService.CurrentUserId = person.Id; - - var httpMethod = new HttpMethod("GET"); - var route = $"/api/v1/todo-items?include=owner"; - - var request = new HttpRequestMessage(httpMethod, route); - - // act - var response = await client.SendAsync(request); - var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService().DeserializeList(responseBody); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - foreach(var item in deserializedBody) - Assert.Equal(person.Id, item.Owner.Id); - } - - [Fact] - public async Task Sparse_Fields_Works_With_Get_Override() - { - // arrange - var builder = new WebHostBuilder() - .UseStartup(); - var server = new TestServer(builder); - var client = server.CreateClient(); - var context = (AppDbContext)server.Host.Services.GetService(typeof(AppDbContext)); - var jsonApiContext = (IJsonApiContext)server.Host.Services.GetService(typeof(IJsonApiContext)); - - var person = new Person(); - context.People.Add(person); - var todoItem = new TodoItem(); - todoItem.Owner = person; - context.TodoItems.Add(todoItem); - context.SaveChanges(); - - var authService = (IAuthorizationService)server.Host.Services.GetService(typeof(IAuthorizationService)); - authService.CurrentUserId = person.Id; - - var httpMethod = new HttpMethod("GET"); - var route = $"/api/v1/todo-items/{todoItem.Id}?fields[todo-items]=description"; - - var request = new HttpRequestMessage(httpMethod, route); - - // act - var response = await client.SendAsync(request); - var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService().Deserialize(responseBody); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(todoItem.Description, deserializedBody.Description); - - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs index 4f9198619a..5f31c447b6 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs @@ -5,19 +5,18 @@ using Microsoft.AspNetCore.TestHost; using Xunit; using JsonApiDotNetCoreExample.Models; -using Newtonsoft.Json; using JsonApiDotNetCore.Models; using System.Collections; -using JsonApiDotNetCoreExampleTests.Startups; +using JsonApiDotNetCoreExample; namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility { [Collection("WebHostCollection")] public class RequestMetaTests { - private TestFixture _fixture; + private TestFixture _fixture; - public RequestMetaTests(TestFixture fixture) + public RequestMetaTests(TestFixture fixture) { _fixture = fixture; } @@ -25,9 +24,7 @@ public RequestMetaTests(TestFixture fixture) [Fact] public async Task Injecting_IRequestMeta_Adds_Meta_Data() { - // arrange - var person = new Person(); - var expectedMeta = person.GetMeta(null); + // Arrange var builder = new WebHostBuilder() .UseStartup(); @@ -37,31 +34,33 @@ public async Task Injecting_IRequestMeta_Adds_Meta_Data() var server = new TestServer(builder); var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); + var expectedMeta = (_fixture.GetService>() as IHasMeta).GetMeta(); - // act + // Act var response = await client.SendAsync(request); - var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - - // assert + var body = await response.Content.ReadAsStringAsync(); + var meta = _fixture.GetDeserializer().DeserializeList(body).Meta; + + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(documents.Meta); + Assert.NotNull(meta); Assert.NotNull(expectedMeta); Assert.NotEmpty(expectedMeta); - - foreach(var hash in expectedMeta) + + foreach (var hash in expectedMeta) { - if(hash.Value is IList) + if (hash.Value is IList) { var listValue = (IList)hash.Value; - for(var i=0; i < listValue.Count; i++) - Assert.Equal(listValue[i].ToString(), ((IList)documents.Meta[hash.Key])[i].ToString()); + for (var i = 0; i < listValue.Count; i++) + Assert.Equal(listValue[i].ToString(), ((IList)meta[hash.Key])[i].ToString()); } else { - Assert.Equal(hash.Value, documents.Meta[hash.Key]); + Assert.Equal(hash.Value, meta[hash.Key]); } } - Assert.Equal("request-meta-value", documents.Meta["request-meta"]); + Assert.Equal("request-meta-value", meta["request-meta"]); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs index 90496b3690..0d3de444ee 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http; using System.Threading.Tasks; using JsonApiDotNetCoreExample; @@ -14,56 +14,56 @@ public class HttpReadOnlyTests [Fact] public async Task Allows_GET_Requests() { - // arrange + // Arrange const string route = "readonly"; const string method = "GET"; - // act + // Act var statusCode = await MakeRequestAsync(route, method); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, statusCode); } [Fact] public async Task Rejects_POST_Requests() { - // arrange + // Arrange const string route = "readonly"; const string method = "POST"; - // act + // Act var statusCode = await MakeRequestAsync(route, method); - // assert + // Assert Assert.Equal(HttpStatusCode.MethodNotAllowed, statusCode); } [Fact] public async Task Rejects_PATCH_Requests() { - // arrange + // Arrange const string route = "readonly"; const string method = "PATCH"; - // act + // Act var statusCode = await MakeRequestAsync(route, method); - // assert + // Assert Assert.Equal(HttpStatusCode.MethodNotAllowed, statusCode); } [Fact] public async Task Rejects_DELETE_Requests() { - // arrange + // Arrange const string route = "readonly"; const string method = "DELETE"; - // act + // Act var statusCode = await MakeRequestAsync(route, method); - // assert + // Assert Assert.Equal(HttpStatusCode.MethodNotAllowed, statusCode); } @@ -79,4 +79,4 @@ private async Task MakeRequestAsync(string route, string method) return response.StatusCode; } } -} \ No newline at end of file +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpDeleteTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpDeleteTests.cs index 32e7eaf109..e7aa542d9a 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpDeleteTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpDeleteTests.cs @@ -14,56 +14,56 @@ public class nohttpdeleteTests [Fact] public async Task Allows_GET_Requests() { - // arrange + // Arrange const string route = "nohttpdelete"; const string method = "GET"; - // act + // Act var statusCode = await MakeRequestAsync(route, method); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, statusCode); } [Fact] public async Task Allows_POST_Requests() { - // arrange + // Arrange const string route = "nohttpdelete"; const string method = "POST"; - // act + // Act var statusCode = await MakeRequestAsync(route, method); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, statusCode); } [Fact] public async Task Allows_PATCH_Requests() { - // arrange + // Arrange const string route = "nohttpdelete"; const string method = "PATCH"; - // act + // Act var statusCode = await MakeRequestAsync(route, method); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, statusCode); } [Fact] public async Task Rejects_DELETE_Requests() { - // arrange + // Arrange const string route = "nohttpdelete"; const string method = "DELETE"; - // act + // Act var statusCode = await MakeRequestAsync(route, method); - // assert + // Assert Assert.Equal(HttpStatusCode.MethodNotAllowed, statusCode); } @@ -79,4 +79,4 @@ private async Task MakeRequestAsync(string route, string method) return response.StatusCode; } } -} \ No newline at end of file +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPatchTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPatchTests.cs index 5b8a33f16a..573b8dccf7 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPatchTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPatchTests.cs @@ -14,56 +14,56 @@ public class nohttppatchTests [Fact] public async Task Allows_GET_Requests() { - // arrange + // Arrange const string route = "nohttppatch"; const string method = "GET"; - // act + // Act var statusCode = await MakeRequestAsync(route, method); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, statusCode); } [Fact] public async Task Allows_POST_Requests() { - // arrange + // Arrange const string route = "nohttppatch"; const string method = "POST"; - // act + // Act var statusCode = await MakeRequestAsync(route, method); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, statusCode); } [Fact] public async Task Rejects_PATCH_Requests() { - // arrange + // Arrange const string route = "nohttppatch"; const string method = "PATCH"; - // act + // Act var statusCode = await MakeRequestAsync(route, method); - // assert + // Assert Assert.Equal(HttpStatusCode.MethodNotAllowed, statusCode); } [Fact] public async Task Allows_DELETE_Requests() { - // arrange + // Arrange const string route = "nohttppatch"; const string method = "DELETE"; - // act + // Act var statusCode = await MakeRequestAsync(route, method); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, statusCode); } @@ -79,4 +79,4 @@ private async Task MakeRequestAsync(string route, string method) return response.StatusCode; } } -} \ No newline at end of file +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPostTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPostTests.cs index f68a65a037..b1dda90c29 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPostTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPostTests.cs @@ -14,56 +14,56 @@ public class NoHttpPostTests [Fact] public async Task Allows_GET_Requests() { - // arrange + // Arrange const string route = "nohttppost"; const string method = "GET"; - // act + // Act var statusCode = await MakeRequestAsync(route, method); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, statusCode); } [Fact] public async Task Rejects_POST_Requests() { - // arrange + // Arrange const string route = "nohttppost"; const string method = "POST"; - // act + // Act var statusCode = await MakeRequestAsync(route, method); - // assert + // Assert Assert.Equal(HttpStatusCode.MethodNotAllowed, statusCode); } [Fact] public async Task Allows_PATCH_Requests() { - // arrange + // Arrange const string route = "nohttppost"; const string method = "PATCH"; - // act + // Act var statusCode = await MakeRequestAsync(route, method); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, statusCode); } [Fact] public async Task Allows_DELETE_Requests() { - // arrange + // Arrange const string route = "nohttppost"; const string method = "DELETE"; - // act + // Act var statusCode = await MakeRequestAsync(route, method); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, statusCode); } @@ -79,4 +79,4 @@ private async Task MakeRequestAsync(string route, string method) return response.StatusCode; } } -} \ No newline at end of file +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs index 5fc6ce902c..1df5593fb0 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -7,9 +6,11 @@ using System.Threading.Tasks; using Bogus; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using Xunit; @@ -25,8 +26,8 @@ public class ManyToManyTests private static readonly Faker _tagFaker = new Faker().RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10)); - private TestFixture _fixture; - public ManyToManyTests(TestFixture fixture) + private TestFixture _fixture; + public ManyToManyTests(TestFixture fixture) { _fixture = fixture; } @@ -34,7 +35,7 @@ public ManyToManyTests(TestFixture fixture) [Fact] public async Task Can_Fetch_Many_To_Many_Through_All() { - // arrange + // Arrange var context = _fixture.GetService(); var article = _articleFaker.Generate(); var tag = _tagFaker.Generate(); @@ -49,22 +50,27 @@ public async Task Can_Fetch_Many_To_Many_Through_All() }; context.ArticleTags.Add(articleTag); await context.SaveChangesAsync(); - var route = $"/api/v1/articles?include=tags"; - // act - var response = await _fixture.Client.GetAsync(route); + // @TODO - Use fixture + var builder = new WebHostBuilder() + .UseStartup(); + var server = new TestServer(builder); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync(route); - // assert + // Assert var body = await response.Content.ReadAsStringAsync(); Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}"); - var document = JsonConvert.DeserializeObject(body); + var document = JsonConvert.DeserializeObject(body); Assert.NotEmpty(document.Included); - var articleResponseList = _fixture.GetService().DeserializeList
(body); + var articleResponseList = _fixture.GetDeserializer().DeserializeList
(body).Data; Assert.NotNull(articleResponseList); - + var articleResponse = articleResponseList.FirstOrDefault(a => a.Id == article.Id); Assert.NotNull(articleResponse); Assert.Equal(article.Name, articleResponse.Name); @@ -77,7 +83,7 @@ public async Task Can_Fetch_Many_To_Many_Through_All() [Fact] public async Task Can_Fetch_Many_To_Many_Through_GetById() { - // arrange + // Arrange var context = _fixture.GetService(); var article = _articleFaker.Generate(); var tag = _tagFaker.Generate(); @@ -91,17 +97,23 @@ public async Task Can_Fetch_Many_To_Many_Through_GetById() var route = $"/api/v1/articles/{article.Id}?include=tags"; - // act - var response = await _fixture.Client.GetAsync(route); + // @TODO - Use fixture + var builder = new WebHostBuilder() + .UseStartup(); + var server = new TestServer(builder); + var client = server.CreateClient(); - // assert + // Act + var response = await client.GetAsync(route); + + // Assert var body = await response.Content.ReadAsStringAsync(); Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}"); - + var document = JsonConvert.DeserializeObject(body); Assert.NotEmpty(document.Included); - var articleResponse = _fixture.GetService().Deserialize
(body); + var articleResponse = _fixture.GetDeserializer().DeserializeSingle
(body).Data; Assert.NotNull(articleResponse); Assert.Equal(article.Id, articleResponse.Id); @@ -111,9 +123,48 @@ public async Task Can_Fetch_Many_To_Many_Through_GetById() } [Fact] - public async Task Can_Fetch_Many_To_Many_Without_Include() + public async Task Can_Fetch_Many_To_Many_Through_GetById_Relationship_Link() + { + // Arrange + var context = _fixture.GetService(); + var article = _articleFaker.Generate(); + var tag = _tagFaker.Generate(); + var articleTag = new ArticleTag + { + Article = article, + Tag = tag + }; + context.ArticleTags.Add(articleTag); + await context.SaveChangesAsync(); + + var route = $"/api/v1/articles/{article.Id}/tags"; + + // @TODO - Use fixture + var builder = new WebHostBuilder() + .UseStartup(); + var server = new TestServer(builder); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync(route); + + // Assert + var body = await response.Content.ReadAsStringAsync(); + Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}"); + + var document = JsonConvert.DeserializeObject(body); + Assert.Null(document.Included); + + var tagResponse = _fixture.GetDeserializer().DeserializeList(body).Data.First(); + Assert.NotNull(tagResponse); + Assert.Equal(tag.Id, tagResponse.Id); + } + + + [Fact] + public async Task Can_Fetch_Many_To_Many_Through_Relationship_Link() { - // arrange + // Arrange var context = _fixture.GetService(); var article = _articleFaker.Generate(); var tag = _tagFaker.Generate(); @@ -125,23 +176,66 @@ public async Task Can_Fetch_Many_To_Many_Without_Include() context.ArticleTags.Add(articleTag); await context.SaveChangesAsync(); + var route = $"/api/v1/articles/{article.Id}/relationships/tags"; + + // @TODO - Use fixture + var builder = new WebHostBuilder() + .UseStartup(); + var server = new TestServer(builder); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync(route); + + // Assert + var body = await response.Content.ReadAsStringAsync(); + Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}"); + + var document = JsonConvert.DeserializeObject(body); + Assert.Null(document.Included); + + var tagResponse = _fixture.GetDeserializer().DeserializeList(body).Data.First(); + Assert.NotNull(tagResponse); + Assert.Equal(tag.Id, tagResponse.Id); + } + + [Fact] + public async Task Can_Fetch_Many_To_Many_Without_Include() + { + // Arrange + var context = _fixture.GetService(); + var article = _articleFaker.Generate(); + var tag = _tagFaker.Generate(); + var articleTag = new ArticleTag + { + Article = article, + Tag = tag + }; + context.ArticleTags.Add(articleTag); + await context.SaveChangesAsync(); var route = $"/api/v1/articles/{article.Id}"; - // act - var response = await _fixture.Client.GetAsync(route); + // @TODO - Use fixture + var builder = new WebHostBuilder() + .UseStartup(); + var server = new TestServer(builder); + var client = server.CreateClient(); + + // Act + var response = await client.GetAsync(route); - // assert + // Assert var body = await response.Content.ReadAsStringAsync(); Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}"); var document = JsonConvert.DeserializeObject(body); - Assert.Null(document.Data.Relationships["tags"].ManyData); + Assert.Null(document.SingleData.Relationships["tags"].ManyData); } [Fact] public async Task Can_Create_Many_To_Many() { - // arrange + // Arrange var context = _fixture.GetService(); var tag = _tagFaker.Generate(); var author = new Author(); @@ -179,20 +273,25 @@ public async Task Can_Create_Many_To_Many() } } }; - request.Content = new StringContent(JsonConvert.SerializeObject(content)); request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); - // act - var response = await _fixture.Client.SendAsync(request); + // @TODO - Use fixture + var builder = new WebHostBuilder() + .UseStartup(); + var server = new TestServer(builder); + var client = server.CreateClient(); + + // Act + var response = await client.SendAsync(request); - // assert + // Assert var body = await response.Content.ReadAsStringAsync(); Assert.True(HttpStatusCode.Created == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}"); - var articleResponse = _fixture.GetService().Deserialize
(body); + var articleResponse = _fixture.GetDeserializer().DeserializeSingle
(body).Data; Assert.NotNull(articleResponse); - + var persistedArticle = await _fixture.Context.Articles .Include(a => a.ArticleTags) .SingleAsync(a => a.Id == articleResponse.Id); @@ -204,7 +303,7 @@ public async Task Can_Create_Many_To_Many() [Fact] public async Task Can_Update_Many_To_Many() { - // arrange + // Arrange var context = _fixture.GetService(); var tag = _tagFaker.Generate(); var article = _articleFaker.Generate(); @@ -236,16 +335,22 @@ public async Task Can_Update_Many_To_Many() request.Content = new StringContent(JsonConvert.SerializeObject(content)); request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); - // act - var response = await _fixture.Client.SendAsync(request); + // @TODO - Use fixture + var builder = new WebHostBuilder() + .UseStartup(); + var server = new TestServer(builder); + var client = server.CreateClient(); + + // Act + var response = await client.SendAsync(request); - // assert + // Assert var body = await response.Content.ReadAsStringAsync(); Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}"); - - var articleResponse = _fixture.GetService().Deserialize
(body); + + var articleResponse = _fixture.GetDeserializer().DeserializeSingle
(body).Data; Assert.NotNull(articleResponse); - + _fixture.ReloadDbContext(); var persistedArticle = await _fixture.Context.Articles .Include(a => a.ArticleTags) @@ -258,7 +363,7 @@ public async Task Can_Update_Many_To_Many() [Fact] public async Task Can_Update_Many_To_Many_With_Complete_Replacement() { - // arrange + // Arrange var context = _fixture.GetService(); var firstTag = _tagFaker.Generate(); var article = _articleFaker.Generate(); @@ -296,14 +401,19 @@ public async Task Can_Update_Many_To_Many_With_Complete_Replacement() request.Content = new StringContent(JsonConvert.SerializeObject(content)); request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); - // act - var response = await _fixture.Client.SendAsync(request); + // @TODO - Use fixture + var builder = new WebHostBuilder().UseStartup(); + var server = new TestServer(builder); + var client = server.CreateClient(); + + // Act + var response = await client.SendAsync(request); - // assert + // Assert var body = await response.Content.ReadAsStringAsync(); Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}"); - var articleResponse = _fixture.GetService().Deserialize
(body); + var articleResponse = _fixture.GetDeserializer().DeserializeSingle
(body).Data; Assert.NotNull(articleResponse); _fixture.ReloadDbContext(); @@ -317,7 +427,7 @@ public async Task Can_Update_Many_To_Many_With_Complete_Replacement() [Fact] public async Task Can_Update_Many_To_Many_With_Complete_Replacement_With_Overlap() { - // arrange + // Arrange var context = _fixture.GetService(); var firstTag = _tagFaker.Generate(); var article = _articleFaker.Generate(); @@ -359,20 +469,25 @@ public async Task Can_Update_Many_To_Many_With_Complete_Replacement_With_Overlap request.Content = new StringContent(JsonConvert.SerializeObject(content)); request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); - // act - var response = await _fixture.Client.SendAsync(request); + // @TODO - Use fixture + var builder = new WebHostBuilder().UseStartup(); + var server = new TestServer(builder); + var client = server.CreateClient(); - // assert + // Act + var response = await client.SendAsync(request); + + // Assert var body = await response.Content.ReadAsStringAsync(); Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}"); - var articleResponse = _fixture.GetService().Deserialize
(body); + var articleResponse = _fixture.GetDeserializer().DeserializeSingle
(body).Data; Assert.NotNull(articleResponse); _fixture.ReloadDbContext(); var persistedArticle = await _fixture.Context.Articles .Include(a => a.ArticleTags) - .SingleOrDefaultAsync( a => a.Id == article.Id); + .SingleOrDefaultAsync(a => a.Id == article.Id); var tags = persistedArticle.ArticleTags.Select(at => at.Tag).ToList(); Assert.Equal(2, tags.Count); } @@ -380,7 +495,7 @@ public async Task Can_Update_Many_To_Many_With_Complete_Replacement_With_Overlap [Fact] public async Task Can_Update_Many_To_Many_Through_Relationship_Link() { - // arrange + // Arrange var context = _fixture.GetService(); var tag = _tagFaker.Generate(); var article = _articleFaker.Generate(); @@ -392,24 +507,29 @@ public async Task Can_Update_Many_To_Many_Through_Relationship_Link() var request = new HttpRequestMessage(new HttpMethod("PATCH"), route); var content = new { - data = new [] { + data = new[] { new { type = "tags", id = tag.StringId - } + } } }; request.Content = new StringContent(JsonConvert.SerializeObject(content)); request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); - // act - var response = await _fixture.Client.SendAsync(request); + // @TODO - Use fixture + var builder = new WebHostBuilder().UseStartup(); + var server = new TestServer(builder); + var client = server.CreateClient(); + + // Act + var response = await client.SendAsync(request); - // assert + // Assert var body = await response.Content.ReadAsStringAsync(); Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}"); - + _fixture.ReloadDbContext(); var persistedArticle = await _fixture.Context.Articles .Include(a => a.ArticleTags) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/QueryFiltersTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/QueryFiltersTests.cs index 92589dd0b5..6c3c23b125 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/QueryFiltersTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/QueryFiltersTests.cs @@ -1,16 +1,13 @@ -using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading.Tasks; using Bogus; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; -using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; using Xunit; namespace JsonApiDotNetCoreExampleTests.Acceptance @@ -18,11 +15,11 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance [Collection("WebHostCollection")] public class QueryFiltersTests { - private TestFixture _fixture; + private TestFixture _fixture; private AppDbContext _context; private Faker _userFaker; - public QueryFiltersTests(TestFixture fixture) + public QueryFiltersTests(TestFixture fixture) { _fixture = fixture; _context = fixture.GetService(); @@ -31,55 +28,65 @@ public QueryFiltersTests(TestFixture fixture) .RuleFor(u => u.Password, f => f.Internet.Password()); } - [Fact] - public async Task FiltersWithCustomQueryFiltersEquals() - { - // Arrange - var user = _userFaker.Generate(); - var firstUsernameCharacter = user.Username[0]; - _context.Users.Add(user); - _context.SaveChanges(); + [Fact] + public async Task FiltersWithCustomQueryFiltersEquals() + { + // Arrange + var user = _userFaker.Generate(); + var firstUsernameCharacter = user.Username[0]; + _context.Users.Add(user); + _context.SaveChanges(); - var httpMethod = new HttpMethod("GET"); - var route = $"/api/v1/users?filter[first-character]=eq:{firstUsernameCharacter}"; - var request = new HttpRequestMessage(httpMethod, route); + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/users?filter[first-character]=eq:{firstUsernameCharacter}"; + var request = new HttpRequestMessage(httpMethod, route); - // Act - var response = await _fixture.Client.SendAsync(request); + // @TODO - Use fixture + var builder = new WebHostBuilder().UseStartup(); + var server = new TestServer(builder); + var client = server.CreateClient(); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService().DeserializeList(body); - var usersWithFirstCharacter = _context.Users.Where(u => u.Username[0] == firstUsernameCharacter); - Assert.True(deserializedBody.All(u => u.Username[0] == firstUsernameCharacter)); - } + // Act + var response = await client.SendAsync(request); - [Fact] - public async Task FiltersWithCustomQueryFiltersLessThan() - { - // Arrange - var aUser = _userFaker.Generate(); - aUser.Username = "alfred"; - var zUser = _userFaker.Generate(); - zUser.Username = "zac"; - _context.Users.AddRange(aUser, zUser); - _context.SaveChanges(); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var deserializedBody = _fixture.GetDeserializer().DeserializeList(body).Data; + var usersWithFirstCharacter = _context.Users.Where(u => u.Username[0] == firstUsernameCharacter); + Assert.True(deserializedBody.All(u => u.Username[0] == firstUsernameCharacter)); + } - var median = 'h'; + [Fact] + public async Task FiltersWithCustomQueryFiltersLessThan() + { + // Arrange + var aUser = _userFaker.Generate(); + aUser.Username = "alfred"; + var zUser = _userFaker.Generate(); + zUser.Username = "zac"; + _context.Users.AddRange(aUser, zUser); + _context.SaveChanges(); - var httpMethod = new HttpMethod("GET"); - var route = $"/api/v1/users?filter[first-character]=lt:{median}"; - var request = new HttpRequestMessage(httpMethod, route); + var median = 'h'; - // Act - var response = await _fixture.Client.SendAsync(request); + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/users?filter[first-character]=lt:{median}"; + var request = new HttpRequestMessage(httpMethod, route); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService().DeserializeList(body); - Assert.True(deserializedBody.All(u => u.Username[0] < median)); - } + // @TODO - Use fixture + var builder = new WebHostBuilder().UseStartup(); + var server = new TestServer(builder); + var client = server.CreateClient(); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var deserializedBody = _fixture.GetDeserializer().DeserializeList(body).Data; + Assert.True(deserializedBody.All(u => u.Username[0] < median)); + } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs index 88b98f982a..0ae0806574 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -7,7 +6,7 @@ using System.Threading.Tasks; using Bogus; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Microsoft.EntityFrameworkCore; @@ -20,7 +19,7 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance [Collection("WebHostCollection")] public class ResourceDefinitionTests { - private TestFixture _fixture; + private TestFixture _fixture; private AppDbContext _context; private Faker _userFaker; private Faker _todoItemFaker; @@ -30,7 +29,7 @@ public class ResourceDefinitionTests .RuleFor(a => a.Author, f => new Author()); private static readonly Faker _tagFaker = new Faker().RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10)); - public ResourceDefinitionTests(TestFixture fixture) + public ResourceDefinitionTests(TestFixture fixture) { _fixture = fixture; _context = fixture.GetService(); @@ -65,7 +64,7 @@ public async Task Password_Is_Not_Included_In_Response_Payload() Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); var document = JsonConvert.DeserializeObject(body); - Assert.False(document.Data.Attributes.ContainsKey("password")); + Assert.False(document.SingleData.Attributes.ContainsKey("password")); } [Fact] @@ -73,24 +72,14 @@ public async Task Can_Create_User_With_Password() { // Arrange var user = _userFaker.Generate(); - var content = new - { - data = new - { - type = "users", - attributes = new Dictionary() - { - { "username", user.Username }, - { "password", user.Password }, - } - } - }; + var serializer = _fixture.GetSerializer(p => new { p.Password, p.Username }); + var httpMethod = new HttpMethod("POST"); var route = $"/api/v1/users"; var request = new HttpRequestMessage(httpMethod, route); - request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content = new StringContent(serializer.Serialize(user)); request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); // Act @@ -101,13 +90,13 @@ public async Task Can_Create_User_With_Password() // response assertions var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (User)_fixture.GetService().Deserialize(body); + var returnedUser = _fixture.GetDeserializer().DeserializeSingle(body).Data; var document = JsonConvert.DeserializeObject(body); - Assert.False(document.Data.Attributes.ContainsKey("password")); - Assert.Equal(user.Username, document.Data.Attributes["username"]); + Assert.False(document.SingleData.Attributes.ContainsKey("password")); + Assert.Equal(user.Username, document.SingleData.Attributes["username"]); // db assertions - var dbUser = await _context.Users.FindAsync(deserializedBody.Id); + var dbUser = await _context.Users.FindAsync(returnedUser.Id); Assert.Equal(user.Username, dbUser.Username); Assert.Equal(user.Password, dbUser.Password); } @@ -119,27 +108,12 @@ public async Task Can_Update_User_Password() var user = _userFaker.Generate(); _context.Users.Add(user); _context.SaveChanges(); - - var newPassword = _userFaker.Generate().Password; - - var content = new - { - data = new - { - type = "users", - id = user.Id, - attributes = new Dictionary() - { - { "password", newPassword }, - } - } - }; - + user.Password = _userFaker.Generate().Password; + var serializer = _fixture.GetSerializer(p => new { p.Password }); var httpMethod = new HttpMethod("PATCH"); var route = $"/api/v1/users/{user.Id}"; - var request = new HttpRequestMessage(httpMethod, route); - request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content = new StringContent(serializer.Serialize(user)); request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); // Act @@ -150,14 +124,14 @@ public async Task Can_Update_User_Password() // response assertions var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (User)_fixture.GetService().Deserialize(body); + var returnedUser = _fixture.GetDeserializer().DeserializeSingle(body).Data; var document = JsonConvert.DeserializeObject(body); - Assert.False(document.Data.Attributes.ContainsKey("password")); - Assert.Equal(user.Username, document.Data.Attributes["username"]); + Assert.False(document.SingleData.Attributes.ContainsKey("password")); + Assert.Equal(user.Username, document.SingleData.Attributes["username"]); // db assertions var dbUser = _context.Users.AsNoTracking().Single(u => u.Id == user.Id); - Assert.Equal(newPassword, dbUser.Password); + Assert.Equal(user.Password, dbUser.Password); } [Fact] @@ -206,10 +180,6 @@ public async Task Unauthorized_Article() var route = $"/api/v1/articles/{article.Id}"; - var httpMethod = new HttpMethod("GET"); - var request = new HttpRequestMessage(httpMethod, route); - - // Act var response = await _fixture.Client.GetAsync(route); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs index 591dfa4c7f..25fd53c6f3 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Bogus; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Newtonsoft.Json; @@ -18,11 +18,11 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec [Collection("WebHostCollection")] public class AttributeFilterTests { - private TestFixture _fixture; + private TestFixture _fixture; private Faker _todoItemFaker; private readonly Faker _personFaker; - public AttributeFilterTests(TestFixture fixture) + public AttributeFilterTests(TestFixture fixture) { _fixture = fixture; _todoItemFaker = new Faker() @@ -38,7 +38,7 @@ public AttributeFilterTests(TestFixture fixture) [Fact] public async Task Can_Filter_On_Guid_Properties() { - // arrange + // Arrange var context = _fixture.GetService(); var todoItem = _todoItemFaker.Generate(); context.TodoItems.Add(todoItem); @@ -48,16 +48,15 @@ public async Task Can_Filter_On_Guid_Properties() var route = $"/api/v1/todo-items?filter[guid-property]={todoItem.GuidProperty}"; var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture - .GetService() - .DeserializeList(body); + var list = _fixture.GetDeserializer().DeserializeList(body).Data; + - var todoItemResponse = deserializedBody.Single(); + var todoItemResponse = list.Single(); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(todoItem.Id, todoItemResponse.Id); Assert.Equal(todoItem.GuidProperty, todoItemResponse.GuidProperty); @@ -66,7 +65,7 @@ public async Task Can_Filter_On_Guid_Properties() [Fact] public async Task Can_Filter_On_Related_Attrs() { - // arrange + // Arrange var context = _fixture.GetService(); var person = _personFaker.Generate(); var todoItem = _todoItemFaker.Generate(); @@ -78,39 +77,36 @@ public async Task Can_Filter_On_Related_Attrs() var route = $"/api/v1/todo-items?include=owner&filter[owner.first-name]={person.FirstName}"; var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - var included = documents.Included; + var list = _fixture.GetDeserializer().DeserializeList(body).Data.First(); + - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(included); - Assert.NotEmpty(included); - foreach (var item in included) - Assert.Equal(person.FirstName, item.Attributes["first-name"]); + list.Owner.FirstName = person.FirstName; } [Fact] public async Task Cannot_Filter_If_Explicitly_Forbidden() { - // arrange + // Arrange var httpMethod = new HttpMethod("GET"); var route = $"/api/v1/todo-items?include=owner&filter[achieved-date]={DateTime.UtcNow.Date}"; var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await _fixture.Client.SendAsync(request); - // assert + // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task Can_Filter_On_Not_Equal_Values() { - // arrange + // Arrange var context = _fixture.GetService(); var todoItem = _todoItemFaker.Generate(); context.TodoItems.Add(todoItem); @@ -121,22 +117,20 @@ public async Task Can_Filter_On_Not_Equal_Values() var route = $"/api/v1/todo-items?page[size]={totalCount}&filter[ordinal]=ne:{todoItem.Ordinal}"; var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedTodoItems = _fixture - .GetService() - .DeserializeList(body); + var list = _fixture.GetDeserializer().DeserializeList(body).Data; - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.DoesNotContain(deserializedTodoItems, x => x.Ordinal == todoItem.Ordinal); + Assert.DoesNotContain(list, x => x.Ordinal == todoItem.Ordinal); } [Fact] public async Task Can_Filter_On_In_Array_Values() { - // arrange + // Arrange var context = _fixture.GetService(); var todoItems = _todoItemFaker.Generate(5); var guids = new List(); @@ -157,14 +151,14 @@ public async Task Can_Filter_On_In_Array_Values() var route = $"/api/v1/todo-items?filter[guid-property]=in:{string.Join(",", guids)}"; var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); var deserializedTodoItems = _fixture - .GetService() - .DeserializeList(body); + .GetDeserializer() + .DeserializeList(body).Data; - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(guids.Count(), deserializedTodoItems.Count()); foreach (var item in deserializedTodoItems) @@ -177,7 +171,7 @@ public async Task Can_Filter_On_In_Array_Values() [Fact] public async Task Can_Filter_On_Related_In_Array_Values() { - // arrange + // Arrange var context = _fixture.GetService(); var todoItems = _todoItemFaker.Generate(3); var ownerFirstNames = new List(); @@ -194,15 +188,15 @@ public async Task Can_Filter_On_Related_In_Array_Values() var route = $"/api/v1/todo-items?include=owner&filter[owner.first-name]=in:{string.Join(",", ownerFirstNames)}"; var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); var included = documents.Included; - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(ownerFirstNames.Count(), documents.Data.Count()); + Assert.Equal(ownerFirstNames.Count(), documents.ManyData.Count()); Assert.NotNull(included); Assert.NotEmpty(included); foreach (var item in included) @@ -213,8 +207,10 @@ public async Task Can_Filter_On_Related_In_Array_Values() [Fact] public async Task Can_Filter_On_Not_In_Array_Values() { - // arrange + // Arrange var context = _fixture.GetService(); + context.TodoItems.RemoveRange(context.TodoItems); + context.SaveChanges(); var todoItems = _todoItemFaker.Generate(5); var guids = new List(); var notInGuids = new List(); @@ -234,14 +230,14 @@ public async Task Can_Filter_On_Not_In_Array_Values() var route = $"/api/v1/todo-items?page[size]={totalCount}&filter[guid-property]=nin:{string.Join(",", notInGuids)}"; var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); var deserializedTodoItems = _fixture - .GetService() - .DeserializeList(body); + .GetDeserializer() + .DeserializeList(body).Data; - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(totalCount - notInGuids.Count(), deserializedTodoItems.Count()); foreach (var item in deserializedTodoItems) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeSortTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeSortTests.cs index 4aef3817fe..4b81073a48 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeSortTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeSortTests.cs @@ -1,4 +1,5 @@ -using System.Net; +using JsonApiDotNetCoreExample; +using System.Net; using System.Net.Http; using System.Threading.Tasks; using Xunit; @@ -8,9 +9,9 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec [Collection("WebHostCollection")] public class AttributeSortTests { - private TestFixture _fixture; + private TestFixture _fixture; - public AttributeSortTests(TestFixture fixture) + public AttributeSortTests(TestFixture fixture) { _fixture = fixture; } @@ -18,15 +19,15 @@ public AttributeSortTests(TestFixture fixture) [Fact] public async Task Cannot_Sort_If_Explicitly_Forbidden() { - // arrange + // Arrange var httpMethod = new HttpMethod("GET"); var route = $"/api/v1/todo-items?include=owner&sort=achieved-date"; var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await _fixture.Client.SendAsync(request); - // assert + // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ContentNegotiation.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ContentNegotiation.cs index 76f5fa4aa7..9397af4dce 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ContentNegotiation.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ContentNegotiation.cs @@ -12,8 +12,8 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec [Collection("WebHostCollection")] public class ContentNegotiation { - private TestFixture _fixture; - public ContentNegotiation(TestFixture fixture) + private TestFixture _fixture; + public ContentNegotiation(TestFixture fixture) { _fixture = fixture; } @@ -21,7 +21,7 @@ public ContentNegotiation(TestFixture fixture) [Fact] public async Task Server_Sends_Correct_ContentType_Header() { - // arrange + // Arrange var builder = new WebHostBuilder() .UseStartup(); var httpMethod = new HttpMethod("GET"); @@ -30,10 +30,10 @@ public async Task Server_Sends_Correct_ContentType_Header() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal("application/vnd.api+json", response.Content.Headers.ContentType.ToString()); } @@ -41,7 +41,7 @@ public async Task Server_Sends_Correct_ContentType_Header() [Fact] public async Task Server_Responds_415_With_MediaType_Parameters() { - // arrange + // Arrange var builder = new WebHostBuilder() .UseStartup(); var httpMethod = new HttpMethod("GET"); @@ -53,17 +53,17 @@ public async Task Server_Responds_415_With_MediaType_Parameters() request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); request.Content.Headers.ContentType.CharSet = "ISO-8859-4"; - // act + // Act var response = await client.SendAsync(request); - // assert + // Assert Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode); } [Fact] public async Task ServerResponds_406_If_RequestAcceptHeader_Contains_MediaTypeParameters() { - // arrange + // Arrange var builder = new WebHostBuilder() .UseStartup(); var httpMethod = new HttpMethod("GET"); @@ -77,10 +77,10 @@ public async Task ServerResponds_406_If_RequestAcceptHeader_Contains_MediaTypePa .Add(acceptHeader); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); - // assert + // Assert Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode); } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs index 93137b4ee2..32ac423eee 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs @@ -1,38 +1,31 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; using System.Threading.Tasks; using Bogus; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Services; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Serialization.Client; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; -using JsonApiDotNetCoreExampleTests.Startups; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; +using JsonApiDotNetCoreExampleTests.Helpers.Models; using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; using Xunit; using Person = JsonApiDotNetCoreExample.Models.Person; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec { + [Collection("WebHostCollection")] - public class CreatingDataTests + public class CreatingDataTests : EndToEndTest { - private TestFixture _fixture; - private IJsonApiContext _jsonApiContext; - private Faker _todoItemFaker; - private Faker _personFaker; + private readonly Faker _todoItemFaker; + private readonly Faker _personFaker; - public CreatingDataTests(TestFixture fixture) + public CreatingDataTests(TestFixture fixture) : base(fixture) { _fixture = fixture; - _jsonApiContext = fixture.GetService(); _todoItemFaker = new Faker() .RuleFor(t => t.Description, f => f.Lorem.Sentence()) .RuleFor(t => t.Ordinal, f => f.Random.Number()) @@ -44,661 +37,311 @@ public CreatingDataTests(TestFixture fixture) } [Fact] - public async Task Can_Create_Guid_Identifiable_Entity() + public async Task CreateResource_GuidResource_IsCreated() { - // arrange - var builder = new WebHostBuilder() - .UseStartup(); - var httpMethod = new HttpMethod("POST"); - var server = new TestServer(builder); - var client = server.CreateClient(); - - var context = _fixture.GetService(); - - var owner = new JsonApiDotNetCoreExample.Models.Person(); - context.People.Add(owner); - await context.SaveChangesAsync(); + // Arrange + var dbContext = PrepareTest(); + var serializer = GetSerializer(e => new { }, e => new { e.Owner }); - var route = "/api/v1/todo-collections"; - var request = new HttpRequestMessage(httpMethod, route); - var content = new - { - data = new - { - type = "todo-collections", - relationships = new - { - owner = new - { - data = new - { - type = "people", - id = owner.Id.ToString() - } - } - } - } - }; - request.Content = new StringContent(JsonConvert.SerializeObject(content)); - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + var owner = new Person(); + dbContext.People.Add(owner); + dbContext.SaveChanges(); + var todoItemCollection = new TodoItemCollection { Owner = owner }; - // act - var response = await client.SendAsync(request); + // Act + var (body, response) = await Post("/api/v1/todo-collections", serializer.Serialize(todoItemCollection)); - // assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); + // Assert + AssertEqualStatusCode(HttpStatusCode.Created, response); } [Fact] - public async Task Cannot_Create_Entity_With_Client_Generate_Id() + public async Task ClientGeneratedId_IntegerIdAndNotEnabled_IsForbidden() { - // arrange - var context = _fixture.GetService(); - var builder = new WebHostBuilder() - .UseStartup(); - var httpMethod = new HttpMethod("POST"); - var route = "/api/v1/todo-items"; - var server = new TestServer(builder); - var client = server.CreateClient(); - var request = new HttpRequestMessage(httpMethod, route); + // Arrange + var dbContext = PrepareTest(); + var serializer = GetSerializer(e => new { e.Description, e.Ordinal, e.CreatedDate }); + var todoItem = _todoItemFaker.Generate(); const int clientDefinedId = 9999; - var content = new - { - data = new - { - type = "todo-items", - id = $"{clientDefinedId}", - attributes = new - { - description = todoItem.Description, - ordinal = todoItem.Ordinal, - createdDate = DateTime.Now - } - } - }; - - request.Content = new StringContent(JsonConvert.SerializeObject(content)); - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + todoItem.Id = clientDefinedId; - // act - var response = await client.SendAsync(request); + // Act + var (body, response) = await Post("/api/v1/todo-items", serializer.Serialize(todoItem)); - // assert - Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + // Assert + AssertEqualStatusCode(HttpStatusCode.Forbidden, response); } + [Fact] - public async Task Can_Create_Entity_With_Client_Defined_Id_If_Configured() + public async Task ClientGeneratedId_IntegerIdAndEnabled_IsCreated() { - // arrange - var context = _fixture.GetService(); - var builder = new WebHostBuilder() - .UseStartup(); - var httpMethod = new HttpMethod("POST"); - var route = "/api/v1/todo-items"; - var server = new TestServer(builder); - var client = server.CreateClient(); - var request = new HttpRequestMessage(httpMethod, route); + // Arrange + var dbContext = PrepareTest(); + var serializer = GetSerializer(e => new { e.Description, e.Ordinal, e.CreatedDate }); + var todoItem = _todoItemFaker.Generate(); const int clientDefinedId = 9999; - var content = new - { - data = new - { - type = "todo-items", - id = $"{clientDefinedId}", - attributes = new - { - description = todoItem.Description, - ordinal = todoItem.Ordinal, - createdDate = DateTime.Now - } - } - }; - - request.Content = new StringContent(JsonConvert.SerializeObject(content)); - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + todoItem.Id = clientDefinedId; - // act - var response = await client.SendAsync(request); - var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); + // Act + var (body, response) = await Post("/api/v1/todo-items", serializer.Serialize(todoItem)); + var responseItem = _deserializer.DeserializeSingle(body).Data; - // assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - Assert.Equal(clientDefinedId, deserializedBody.Id); + // Assert + AssertEqualStatusCode(HttpStatusCode.Created, response); + Assert.Equal(clientDefinedId, responseItem.Id); } - [Fact] - public async Task Can_Create_Guid_Identifiable_Entity_With_Client_Defined_Id_If_Configured() + public async Task ClientGeneratedId_GuidIdAndEnabled_IsCreated() { - // arrange - var builder = new WebHostBuilder() - .UseStartup(); - var httpMethod = new HttpMethod("POST"); - var server = new TestServer(builder); - var client = server.CreateClient(); - - var context = _fixture.GetService(); - - var owner = new JsonApiDotNetCoreExample.Models.Person(); - context.People.Add(owner); - await context.SaveChangesAsync(); + // Arrange + var dbContext = PrepareTest(); + var serializer = GetSerializer(e => new { }, e => new { e.Owner }); - var route = "/api/v1/todo-collections"; - var request = new HttpRequestMessage(httpMethod, route); + var owner = new Person(); + dbContext.People.Add(owner); + await dbContext.SaveChangesAsync(); var clientDefinedId = Guid.NewGuid(); - var content = new - { - data = new - { - type = "todo-collections", - id = $"{clientDefinedId}", - relationships = new - { - owner = new - { - data = new - { - type = "people", - id = owner.Id.ToString() - } - } - } - } - }; - request.Content = new StringContent(JsonConvert.SerializeObject(content)); - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + var todoItemCollection = new TodoItemCollection { Owner = owner, OwnerId = owner.Id, Id = clientDefinedId }; - // act - var response = await client.SendAsync(request); - var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItemCollection)_fixture.GetService().Deserialize(body); + // Act + var (body, response) = await Post("/api/v1/todo-collections", serializer.Serialize(todoItemCollection)); + var responseItem = _deserializer.DeserializeSingle(body).Data; - // assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - Assert.Equal(clientDefinedId, deserializedBody.Id); + // Assert + AssertEqualStatusCode(HttpStatusCode.Created, response); + Assert.Equal(clientDefinedId, responseItem.Id); } [Fact] - public async Task Can_Create_And_Set_HasMany_Relationships() + public async Task CreateWithRelationship_HasMany_IsCreated() { - // arrange - var builder = new WebHostBuilder() - .UseStartup(); - var httpMethod = new HttpMethod("POST"); - var server = new TestServer(builder); - var client = server.CreateClient(); - - var context = _fixture.GetService(); - - var owner = new JsonApiDotNetCoreExample.Models.Person(); - var todoItem = new TodoItem(); - todoItem.Owner = owner; - context.People.Add(owner); - context.TodoItems.Add(todoItem); - await context.SaveChangesAsync(); - - var route = "/api/v1/todo-collections"; - var request = new HttpRequestMessage(httpMethod, route); - var content = new - { - data = new - { - type = "todo-collections", - relationships = new Dictionary - { - { "owner", new { - data = new - { - type = "people", - id = owner.Id.ToString() - } - } }, - { "todo-items", new { - data = new dynamic[] - { - new { - type = "todo-items", - id = todoItem.Id.ToString() - } - } - } } - } - } - }; + // Arrange + var dbContext = PrepareTest(); + var serializer = GetSerializer(e => new { }, e => new { e.TodoItems }); - request.Content = new StringContent(JsonConvert.SerializeObject(content)); - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + var todoItem = _todoItemFaker.Generate(); + dbContext.TodoItems.Add(todoItem); + dbContext.SaveChanges(); + var todoCollection = new TodoItemCollection { TodoItems = new List { todoItem } }; - // act - var response = await client.SendAsync(request); - var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItemCollection)_fixture.GetService().Deserialize(body); - var newId = deserializedBody.Id; + // Act + var (body, response) = await Post("/api/v1/todo-collections", serializer.Serialize(todoCollection)); + var responseItem = _deserializer.DeserializeSingle(body).Data; - context = _fixture.GetService(); - var contextCollection = context.TodoItemCollections + // Assert + var contextCollection = GetDbContext().TodoItemCollections.AsNoTracking() .Include(c => c.Owner) .Include(c => c.TodoItems) - .SingleOrDefault(c => c.Id == newId); + .SingleOrDefault(c => c.Id == responseItem.Id); - // assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - Assert.Equal(owner.Id, contextCollection.OwnerId); + AssertEqualStatusCode(HttpStatusCode.Created, response); Assert.NotEmpty(contextCollection.TodoItems); + Assert.Equal(todoItem.Id, contextCollection.TodoItems.First().Id); } [Fact] - public async Task Can_Create_With_HasMany_Relationship_And_Include_Result() + public async Task CreateWithRelationship_HasManyAndInclude_IsCreatedAndIncludes() { - // arrange - var builder = new WebHostBuilder() - .UseStartup(); - var httpMethod = new HttpMethod("POST"); - var server = new TestServer(builder); - var client = server.CreateClient(); - - var context = _fixture.GetService(); + // Arrange + var dbContext = PrepareTest(); + var serializer = GetSerializer(e => new { }, e => new { e.TodoItems, e.Owner }); - var owner = new JsonApiDotNetCoreExample.Models.Person(); - var todoItem = new TodoItem(); - todoItem.Owner = owner; - todoItem.Description = "Description"; - context.People.Add(owner); - context.TodoItems.Add(todoItem); - await context.SaveChangesAsync(); - - var route = "/api/v1/todo-collections?include=todo-items"; - var request = new HttpRequestMessage(httpMethod, route); - var content = new + var owner = new Person(); + var todoItem = new TodoItem { - data = new - { - type = "todo-collections", - relationships = new Dictionary - { - { "owner", new { - data = new - { - type = "people", - id = owner.Id.ToString() - } - } }, - { "todo-items", new { - data = new dynamic[] - { - new { - type = "todo-items", - id = todoItem.Id.ToString() - } - } - } } - } - } + Owner = owner, + Description = "Description" }; + dbContext.People.Add(owner); + dbContext.TodoItems.Add(todoItem); + dbContext.SaveChanges(); + var todoCollection = new TodoItemCollection { Owner = owner, TodoItems = new List { todoItem } }; - request.Content = new StringContent(JsonConvert.SerializeObject(content)); - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); - - // act - var response = await client.SendAsync(request); + // Act + var (body, response) = await Post("/api/v1/todo-collections?include=todo-items", serializer.Serialize(todoCollection)); + var responseItem = _deserializer.DeserializeSingle(body).Data; - // assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - var body = await response.Content.ReadAsStringAsync(); - var collectionResult = _fixture.GetService().Deserialize(body); + // Assert + AssertEqualStatusCode(HttpStatusCode.Created, response); - Assert.NotNull(collectionResult); - Assert.NotEmpty(collectionResult.TodoItems); - Assert.Equal(todoItem.Description, collectionResult.TodoItems.Single().Description); + Assert.NotNull(responseItem); + Assert.NotEmpty(responseItem.TodoItems); + Assert.Equal(todoItem.Description, responseItem.TodoItems.Single().Description); } [Fact] - public async Task Can_Create_And_Set_HasOne_Relationships() + public async Task CreateWithRelationship_HasOne_IsCreated() { - // arrange - var builder = new WebHostBuilder() - .UseStartup(); - var httpMethod = new HttpMethod("POST"); - var server = new TestServer(builder); - var client = server.CreateClient(); - - var context = _fixture.GetService(); + // Arrange + var dbContext = PrepareTest(); + var serializer = GetSerializer(attributes: ti => new { }, relationships: ti => new { ti.Owner }); var todoItem = new TodoItem(); - var owner = new JsonApiDotNetCoreExample.Models.Person(); - context.People.Add(owner); - await context.SaveChangesAsync(); - - var route = "/api/v1/todo-items"; - var request = new HttpRequestMessage(httpMethod, route); - var content = new - { - data = new - { - type = "todo-items", - relationships = new Dictionary - { - { "owner", new { - data = new - { - type = "people", - id = owner.Id.ToString() - } - } } - } - } - }; - - request.Content = new StringContent(JsonConvert.SerializeObject(content)); - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); - - // act - var response = await client.SendAsync(request); - var body = await response.Content.ReadAsStringAsync(); + var owner = new Person(); + dbContext.People.Add(owner); + await dbContext.SaveChangesAsync(); + todoItem.Owner = owner; - // assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); - var newId = deserializedBody.Id; + // Act + var (body, response) = await Post("/api/v1/todo-items", serializer.Serialize(todoItem)); + var responseItem = _deserializer.DeserializeSingle(body).Data; - context = _fixture.GetService(); - var todoItemResult = context.TodoItems + // Assert + var todoItemResult = GetDbContext().TodoItems.AsNoTracking() .Include(c => c.Owner) - .SingleOrDefault(c => c.Id == newId); - + .SingleOrDefault(c => c.Id == responseItem.Id); + AssertEqualStatusCode(HttpStatusCode.Created, response); Assert.Equal(owner.Id, todoItemResult.OwnerId); } [Fact] - public async Task Can_Create_With_HasOne_Relationship_And_Include_Result() + public async Task CreateWithRelationship_HasOneAndInclude_IsCreatedAndIncludes() { - // arrange - var builder = new WebHostBuilder().UseStartup(); - - var httpMethod = new HttpMethod("POST"); - var server = new TestServer(builder); - var client = server.CreateClient(); - - var context = _fixture.GetService(); + // Arrange + var dbContext = PrepareTest(); + var serializer = GetSerializer(attributes: ti => new { }, relationships: ti => new { ti.Owner }); var todoItem = new TodoItem(); - var owner = new JsonApiDotNetCoreExample.Models.Person - { - FirstName = "Alice" - }; - context.People.Add(owner); - - await context.SaveChangesAsync(); - - var route = "/api/v1/todo-items?include=owner"; - var request = new HttpRequestMessage(httpMethod, route); - var content = new - { - data = new - { - type = "todo-items", - relationships = new Dictionary - { - { "owner", new { - data = new - { - type = "people", - id = owner.Id.ToString() - } - } } - } - } - }; - - request.Content = new StringContent(JsonConvert.SerializeObject(content)); - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + var owner = new Person { FirstName = "Alice" }; + dbContext.People.Add(owner); + dbContext.SaveChanges(); + todoItem.Owner = owner; - // act - var response = await client.SendAsync(request); - var body = await response.Content.ReadAsStringAsync(); + // Act + var (body, response) = await Post("/api/v1/todo-items?include=owner", serializer.Serialize(todoItem)); + var responseItem = _deserializer.DeserializeSingle(body).Data; - // assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - var todoItemResult = (TodoItem)_fixture.GetService().Deserialize(body); - Assert.NotNull(todoItemResult); - Assert.NotNull(todoItemResult.Owner); - Assert.Equal(owner.FirstName, todoItemResult.Owner.FirstName); + // Assert + AssertEqualStatusCode(HttpStatusCode.Created, response); + Assert.NotNull(responseItem); + Assert.NotNull(responseItem.Owner); + Assert.Equal(owner.FirstName, responseItem.Owner.FirstName); } [Fact] - public async Task Can_Create_And_Set_HasOne_Relationships_From_Independent_Side() + public async Task CreateWithRelationship_HasOneFromIndependentSide_IsCreated() { - // arrange - var builder = new WebHostBuilder() - .UseStartup(); - var httpMethod = new HttpMethod("POST"); - var server = new TestServer(builder); - var client = server.CreateClient(); - - var context = _fixture.GetService(); - - var person = new JsonApiDotNetCoreExample.Models.Person(); - context.People.Add(person); - await context.SaveChangesAsync(); - - var route = "/api/v1/person-roles"; - var request = new HttpRequestMessage(httpMethod, route); - var clientDefinedId = Guid.NewGuid(); - var content = new - { - data = new - { - type = "person-roles", - relationships = new - { - person = new - { - data = new - { - type = "people", - id = person.Id.ToString() - } - } - } - } - }; + // Arrange + var dbContext = PrepareTest(); + var serializer = GetSerializer(pr => new { }, pr => new { pr.Person }); - request.Content = new StringContent(JsonConvert.SerializeObject(content)); - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + var person = new Person(); + dbContext.People.Add(person); + dbContext.SaveChanges(); + var personRole = new PersonRole { Person = person }; - // act - var response = await client.SendAsync(request); - var body = await response.Content.ReadAsStringAsync(); + // Act + var (body, response) = await Post("/api/v1/person-roles", serializer.Serialize(personRole)); + var responseItem = _deserializer.DeserializeSingle(body).Data; - // assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - var deserializedBody = (PersonRole)_fixture.GetService().Deserialize(body); - Assert.Equal(person.Id, deserializedBody.Person.Id); + // Assert + var personRoleResult = dbContext.PersonRoles.AsNoTracking() + .Include(c => c.Person) + .SingleOrDefault(c => c.Id == responseItem.Id); + AssertEqualStatusCode(HttpStatusCode.Created, response); + Assert.NotEqual(0, responseItem.Id); + Assert.Equal(person.Id, personRoleResult.Person.Id); } [Fact] - public async Task ShouldReceiveLocationHeader_InResponse() + public async Task CreateResource_SimpleResource_HeaderLocationsAreCorrect() { - // arrange - var builder = new WebHostBuilder() - .UseStartup(); - var httpMethod = new HttpMethod("POST"); - var route = "/api/v1/todo-items"; - var server = new TestServer(builder); - var client = server.CreateClient(); - var request = new HttpRequestMessage(httpMethod, route); + // Arrange + var dbContext = PrepareTest(); + var serializer = GetSerializer(ti => new { ti.CreatedDate, ti.Description, ti.Ordinal }); + var todoItem = _todoItemFaker.Generate(); - var content = new - { - data = new - { - type = "todo-items", - attributes = new - { - description = todoItem.Description, - ordinal = todoItem.Ordinal, - createdDate = DateTime.Now - } - } - }; - request.Content = new StringContent(JsonConvert.SerializeObject(content)); - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); - // act - var response = await client.SendAsync(request); - var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); + // Act + var (body, response) = await Post("/api/v1/todo-items", serializer.Serialize(todoItem)); + var responseItem = _deserializer.DeserializeSingle(body).Data; - // assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - Assert.Equal($"/api/v1/todo-items/{deserializedBody.Id}", response.Headers.Location.ToString()); + // Assert + AssertEqualStatusCode(HttpStatusCode.Created, response); + Assert.Equal($"/api/v1/todo-items/{responseItem.Id}", response.Headers.Location.ToString()); } [Fact] - public async Task Respond_409_ToIncorrectEntityType() + public async Task CreateResource_EntityTypeMismatch_IsConflict() { - // arrange - var builder = new WebHostBuilder() - .UseStartup(); - var httpMethod = new HttpMethod("POST"); - var route = "/api/v1/todo-items"; - var server = new TestServer(builder); - var client = server.CreateClient(); - var request = new HttpRequestMessage(httpMethod, route); - var todoItem = _todoItemFaker.Generate(); - var content = new - { - data = new - { - type = "people", - attributes = new - { - description = todoItem.Description, - ordinal = todoItem.Ordinal, - createdDate = DateTime.Now - } - } - }; - request.Content = new StringContent(JsonConvert.SerializeObject(content)); - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + // Arrange + var dbContext = PrepareTest(); + var serializer = GetSerializer(e => new { }, e => new { e.Owner }); + var resourceGraph = new ResourceGraphBuilder().AddResource("todo-items").AddResource().AddResource().Build(); + var _deserializer = new ResponseDeserializer(resourceGraph); - // act - var response = await client.SendAsync(request); + var content = serializer.Serialize(_todoItemFaker.Generate()).Replace("todo-items", "people"); - // assert - Assert.Equal(HttpStatusCode.Conflict, response.StatusCode); + // Act + var (body, response) = await Post("/api/v1/todo-items", content); + + // Assert + AssertEqualStatusCode(HttpStatusCode.Conflict, response); } [Fact] - public async Task Create_With_ToOne_Relationship_With_Implicit_Remove() + public async Task CreateRelationship_ToOneWithImplicitRemove_IsCreated() { // Arrange - var context = _fixture.GetService(); - var passport = new Passport(); - var person1 = _personFaker.Generate(); - person1.Passport = passport; - context.People.AddRange(new List() { person1 }); - await context.SaveChangesAsync(); - var passportId = person1.PassportId; - var content = new - { - data = new - { - type = "people", - attributes = new Dictionary() { { "first-name", "Joe" } }, - relationships = new Dictionary - { - { "passport", new - { - data = new { type = "passports", id = $"{passportId}" } - } - } - } - } - }; - - var httpMethod = new HttpMethod("POST"); - var route = $"/api/v1/people"; - var request = new HttpRequestMessage(httpMethod, route); + var dbContext = PrepareTest(); + var serializer = GetSerializer(e => new { e.FirstName }, e => new { e.Passport }); - string serializedContent = JsonConvert.SerializeObject(content); - request.Content = new StringContent(serializedContent); - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + var passport = new Passport(); + var currentPerson = _personFaker.Generate(); + currentPerson.Passport = passport; + dbContext.People.Add(currentPerson); + dbContext.SaveChanges(); + var newPerson = _personFaker.Generate(); + newPerson.Passport = passport; // Act - var response = await _fixture.Client.SendAsync(request); - var body = await response.Content.ReadAsStringAsync(); - var personResult = _fixture.GetService().Deserialize(body); + var (body, response) = await Post("/api/v1/people", serializer.Serialize(newPerson)); + var responseItem = _deserializer.DeserializeSingle(body).Data; // Assert - - Assert.True(HttpStatusCode.Created == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}"); - var dbPerson = context.People.AsNoTracking().Where(p => p.Id == personResult.Id).Include("Passport").FirstOrDefault(); - Assert.Equal(passportId, dbPerson.Passport.Id); + AssertEqualStatusCode(HttpStatusCode.Created, response); + var newPersonDb = dbContext.People.AsNoTracking().Where(p => p.Id == responseItem.Id).Include(e => e.Passport).Single(); + Assert.NotNull(newPersonDb.Passport); + Assert.Equal(passport.Id, newPersonDb.Passport.Id); } [Fact] - public async Task Create_With_ToMany_Relationship_With_Implicit_Remove() + public async Task CreateRelationship_ToManyWithImplicitRemove_IsCreated() { // Arrange - var context = _fixture.GetService(); - var person1 = _personFaker.Generate(); - person1.TodoItems = _todoItemFaker.Generate(3).ToList(); - context.People.AddRange(new List() { person1 }); - await context.SaveChangesAsync(); - var todoItem1Id = person1.TodoItems[0].Id; - var todoItem2Id = person1.TodoItems[1].Id; - - var content = new - { - data = new - { - type = "people", - attributes = new Dictionary() { { "first-name", "Joe" } }, - relationships = new Dictionary - { - { "todo-items", new - { - data = new List - { - new { - type = "todo-items", - id = $"{todoItem1Id}" - }, - new { - type = "todo-items", - id = $"{todoItem2Id}" - } - } - } - } - } - } - }; - - var httpMethod = new HttpMethod("POST"); - var route = $"/api/v1/people"; - var request = new HttpRequestMessage(httpMethod, route); + var dbContext = PrepareTest(); + var serializer = GetSerializer(e => new { e.FirstName }, e => new { e.TodoItems }); - string serializedContent = JsonConvert.SerializeObject(content); - request.Content = new StringContent(serializedContent); - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + var context = _fixture.GetService(); + var currentPerson = _personFaker.Generate(); + var todoItems = _todoItemFaker.Generate(3).ToList(); + currentPerson.TodoItems = todoItems; + dbContext.Add(currentPerson); + dbContext.SaveChanges(); + var firstTd = currentPerson.TodoItems[0]; + var secondTd = currentPerson.TodoItems[1]; + var thirdTd = currentPerson.TodoItems[2]; + + var newPerson = _personFaker.Generate(); + newPerson.TodoItems = new List { firstTd, secondTd }; // Act - var response = await _fixture.Client.SendAsync(request); - var body = await response.Content.ReadAsStringAsync(); - var personResult = _fixture.GetService().Deserialize(body); + var (body, response) = await Post("/api/v1/people", serializer.Serialize(newPerson)); + var responseItem = _deserializer.DeserializeSingle(body).Data; // Assert - Assert.True(HttpStatusCode.Created == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}"); - var dbPerson = context.People.AsNoTracking().Where(p => p.Id == personResult.Id).Include("TodoItems").FirstOrDefault(); - Assert.Equal(2, dbPerson.TodoItems.Count); - Assert.NotNull(dbPerson.TodoItems.SingleOrDefault(ti => ti.Id == todoItem1Id)); - Assert.NotNull(dbPerson.TodoItems.SingleOrDefault(ti => ti.Id == todoItem2Id)); + var newPersonDb = dbContext.People.AsNoTracking().Where(p => p.Id == responseItem.Id).Include(e => e.TodoItems).Single(); + var oldPersonDb = dbContext.People.AsNoTracking().Where(p => p.Id == currentPerson.Id).Include(e => e.TodoItems).Single(); + AssertEqualStatusCode(HttpStatusCode.Created, response); + Assert.Equal(2, newPersonDb.TodoItems.Count); + Assert.Single(oldPersonDb.TodoItems); + Assert.NotNull(newPersonDb.TodoItems.SingleOrDefault(ti => ti.Id == firstTd.Id)); + Assert.NotNull(newPersonDb.TodoItems.SingleOrDefault(ti => ti.Id == secondTd.Id)); + Assert.NotNull(oldPersonDb.TodoItems.SingleOrDefault(ti => ti.Id == thirdTd.Id)); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs index 5e4754c7c5..2299ee2ba2 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs @@ -1,14 +1,15 @@ +using System; using System.Collections.Generic; using System.Net; -using System.Net.Http; using System.Threading.Tasks; -using Bogus; +using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization.Client; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCoreExampleTests.Helpers.Extensions; -using Microsoft.AspNetCore.Hosting; +using JsonApiDotNetCoreExampleTests.Helpers.Models; using Newtonsoft.Json; using Xunit; using Person = JsonApiDotNetCoreExample.Models.Person; @@ -18,9 +19,9 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec [Collection("WebHostCollection")] public class DeeplyNestedInclusionTests { - private TestFixture _fixture; + private TestFixture _fixture; - public DeeplyNestedInclusionTests(TestFixture fixture) + public DeeplyNestedInclusionTests(TestFixture fixture) { _fixture = fixture; } @@ -36,28 +37,32 @@ private void ResetContext(AppDbContext context) [Fact] public async Task Can_Include_Nested_Relationships() { - // arrange + // Arrange const string route = "/api/v1/todo-items?include=collection.owner"; - - var todoItem = new TodoItem { - Collection = new TodoItemCollection { + var resourceGraph = new ResourceGraphBuilder().AddResource("todo-items").AddResource().AddResource().Build(); + var deserializer = new ResponseDeserializer(resourceGraph); + var todoItem = new TodoItem + { + Collection = new TodoItemCollection + { Owner = new Person() } }; - + var context = _fixture.GetService(); context.TodoItems.RemoveRange(context.TodoItems); context.TodoItems.Add(todoItem); await context.SaveChangesAsync(); - // act + // Act var response = await _fixture.Client.GetAsync(route); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var todoItems = _fixture.DeSerializer.DeserializeList(body); + + var todoItems = deserializer.DeserializeList(body).Data; var responseTodoItem = Assert.Single(todoItems); Assert.NotNull(responseTodoItem); @@ -68,11 +73,13 @@ public async Task Can_Include_Nested_Relationships() [Fact] public async Task Can_Include_Nested_HasMany_Relationships() { - // arrange + // Arrange const string route = "/api/v1/todo-items?include=collection.todo-items"; - var todoItem = new TodoItem { - Collection = new TodoItemCollection { + var todoItem = new TodoItem + { + Collection = new TodoItemCollection + { Owner = new Person(), TodoItems = new List { new TodoItem(), @@ -80,24 +87,24 @@ public async Task Can_Include_Nested_HasMany_Relationships() } } }; - - + + var context = _fixture.GetService(); ResetContext(context); context.TodoItems.Add(todoItem); await context.SaveChangesAsync(); - // act + // Act var response = await _fixture.Client.GetAsync(route); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var documents = JsonConvert.DeserializeObject(body); + var documents = JsonConvert.DeserializeObject(body); var included = documents.Included; - + Assert.Equal(4, included.Count); Assert.Equal(3, included.CountOfType("todo-items")); @@ -107,11 +114,13 @@ public async Task Can_Include_Nested_HasMany_Relationships() [Fact] public async Task Can_Include_Nested_HasMany_Relationships_BelongsTo() { - // arrange + // Arrange const string route = "/api/v1/todo-items?include=collection.todo-items.owner"; - var todoItem = new TodoItem { - Collection = new TodoItemCollection { + var todoItem = new TodoItem + { + Collection = new TodoItemCollection + { Owner = new Person(), TodoItems = new List { new TodoItem { @@ -121,23 +130,23 @@ public async Task Can_Include_Nested_HasMany_Relationships_BelongsTo() } } }; - + var context = _fixture.GetService(); ResetContext(context); context.TodoItems.Add(todoItem); await context.SaveChangesAsync(); - // act + // Act var response = await _fixture.Client.GetAsync(route); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var documents = JsonConvert.DeserializeObject(body); + var documents = JsonConvert.DeserializeObject(body); var included = documents.Included; - + Assert.Equal(5, included.Count); Assert.Equal(3, included.CountOfType("todo-items")); @@ -148,12 +157,15 @@ public async Task Can_Include_Nested_HasMany_Relationships_BelongsTo() [Fact] public async Task Can_Include_Nested_Relationships_With_Multiple_Paths() { - // arrange + // Arrange const string route = "/api/v1/todo-items?include=collection.owner.role,collection.todo-items.owner"; - var todoItem = new TodoItem { - Collection = new TodoItemCollection { - Owner = new Person { + var todoItem = new TodoItem + { + Collection = new TodoItemCollection + { + Owner = new Person + { Role = new PersonRole() }, TodoItems = new List { @@ -164,25 +176,25 @@ public async Task Can_Include_Nested_Relationships_With_Multiple_Paths() } } }; - + var context = _fixture.GetService(); ResetContext(context); context.TodoItems.Add(todoItem); await context.SaveChangesAsync(); - // act + // Act var response = await _fixture.Client.GetAsync(route); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var documents = JsonConvert.DeserializeObject(body); + var documents = JsonConvert.DeserializeObject(body); var included = documents.Included; - + Assert.Equal(7, included.Count); - + Assert.Equal(3, included.CountOfType("todo-items")); Assert.Equal(2, included.CountOfType("people")); Assert.Equal(1, included.CountOfType("person-roles")); @@ -192,7 +204,7 @@ public async Task Can_Include_Nested_Relationships_With_Multiple_Paths() [Fact] public async Task Included_Resources_Are_Correct() { - // arrange + // Arrange var role = new PersonRole(); var assignee = new Person { Role = role }; var collectionOwner = new Person(); @@ -225,10 +237,10 @@ public async Task Included_Resources_Are_Correct() "assignee.role," + "assignee.assigned-todo-items"; - // act + // Act var response = await _fixture.Client.GetAsync(route); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); @@ -269,7 +281,7 @@ public async Task Included_Resources_Are_Correct() [Fact] public async Task Can_Include_Doubly_HasMany_Relationships() { - // arrange + // Arrange var person = new Person { TodoItemCollections = new List { new TodoItemCollection { @@ -297,10 +309,10 @@ public async Task Can_Include_Doubly_HasMany_Relationships() string route = "/api/v1/people/" + person.Id + "?include=todo-collections.todo-items"; - // act + // Act var response = await _fixture.Client.GetAsync(route); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); @@ -313,4 +325,4 @@ public async Task Can_Include_Doubly_HasMany_Relationships() Assert.Equal(2, included.CountOfType("todo-collections")); } } -} \ No newline at end of file +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeletingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeletingDataTests.cs index 8c506f4a33..00abbdec85 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeletingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeletingDataTests.cs @@ -8,6 +8,7 @@ using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; +using Microsoft.EntityFrameworkCore; using Xunit; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec @@ -15,11 +16,11 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec [Collection("WebHostCollection")] public class DeletingDataTests { - private TestFixture _fixture; + private TestFixture _fixture; private AppDbContext _context; private Faker _todoItemFaker; - public DeletingDataTests(TestFixture fixture) + public DeletingDataTests(TestFixture fixture) { _fixture = fixture; _context = fixture.GetService(); @@ -32,9 +33,10 @@ public DeletingDataTests(TestFixture fixture) [Fact] public async Task Respond_404_If_EntityDoesNotExist() { - // arrange - var maxPersonId = _context.TodoItems.LastOrDefault()?.Id ?? 0; - var todoItem = _todoItemFaker.Generate(); + // Arrange + var lastTodo = _context.TodoItems.AsEnumerable().LastOrDefault(); + var lastTodoId = lastTodo?.Id ?? 0; + var builder = new WebHostBuilder() .UseStartup(); @@ -42,7 +44,7 @@ public async Task Respond_404_If_EntityDoesNotExist() var client = server.CreateClient(); var httpMethod = new HttpMethod("DELETE"); - var route = $"/api/v1/todo-items/{maxPersonId + 100}"; + var route = $"/api/v1/todo-items/{lastTodoId + 100}"; var request = new HttpRequestMessage(httpMethod, route); // Act diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs index e277c8d0af..1471b0410b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs @@ -19,15 +19,13 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec.DocumentTests [Collection("WebHostCollection")] public class Included { - private TestFixture _fixture; - private AppDbContext _context; - private Bogus.Faker _personFaker; - private Faker _todoItemFaker; - private Faker _todoItemCollectionFaker; + private readonly AppDbContext _context; + private readonly Bogus.Faker _personFaker; + private readonly Faker _todoItemFaker; + private readonly Faker _todoItemCollectionFaker; - public Included(TestFixture fixture) + public Included(TestFixture fixture) { - _fixture = fixture; _context = fixture.GetService(); _personFaker = new Faker() .RuleFor(p => p.FirstName, f => f.Name.FirstName()) @@ -43,9 +41,9 @@ public Included(TestFixture fixture) } [Fact] - public async Task GET_Included_Contains_SideloadedData_ForManyToOne() + public async Task GET_Included_Contains_SideloadeData_ForManyToOne() { - // arrange + // Arrange var person = _personFaker.Generate(); var todoItem = _todoItemFaker.Generate(); todoItem.Owner = person; @@ -62,24 +60,28 @@ public async Task GET_Included_Contains_SideloadedData_ForManyToOne() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); - // assert + // Assert var json = await response.Content.ReadAsStringAsync(); - var documents = JsonConvert.DeserializeObject(json); + var documents = JsonConvert.DeserializeObject(json); // we only care about counting the todo-items that have owners - var expectedCount = documents.Data.Count(d => d.Relationships["owner"].SingleData != null); + var expectedCount = documents.ManyData.Count(d => d.Relationships["owner"].SingleData != null); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotEmpty(documents.Included); Assert.Equal(expectedCount, documents.Included.Count); + + server.Dispose(); + request.Dispose(); + response.Dispose(); } [Fact] - public async Task GET_ById_Included_Contains_SideloadedData_ForManyToOne() + public async Task GET_ById_Included_Contains_SideloadeData_ForManyToOne() { - // arrange + // Arrange var person = _personFaker.Generate(); var todoItem = _todoItemFaker.Generate(); todoItem.Owner = person; @@ -97,23 +99,27 @@ public async Task GET_ById_Included_Contains_SideloadedData_ForManyToOne() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); var responseString = await response.Content.ReadAsStringAsync(); var document = JsonConvert.DeserializeObject(responseString); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotEmpty(document.Included); Assert.Equal(person.Id.ToString(), document.Included[0].Id); Assert.Equal(person.FirstName, document.Included[0].Attributes["first-name"]); Assert.Equal(person.LastName, document.Included[0].Attributes["last-name"]); + + server.Dispose(); + request.Dispose(); + response.Dispose(); } [Fact] - public async Task GET_Included_Contains_SideloadedData_OneToMany() + public async Task GET_Included_Contains_SideloadeData_OneToMany() { - // arrange + // Arrange _context.People.RemoveRange(_context.People); // ensure all people have todo-items _context.TodoItems.RemoveRange(_context.TodoItems); var person = _personFaker.Generate(); @@ -132,23 +138,28 @@ public async Task GET_Included_Contains_SideloadedData_OneToMany() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); - var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - var data = documents.Data[0]; + var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotEmpty(documents.Included); - Assert.Equal(documents.Data.Count, documents.Included.Count); + Assert.Equal(documents.ManyData.Count, documents.Included.Count); + + server.Dispose(); + request.Dispose(); + response.Dispose(); } [Fact] public async Task GET_Included_DoesNot_Duplicate_Records_ForMultipleRelationshipsOfSameType() { - // arrange - _context.People.RemoveRange(_context.People); // ensure all people have todo-items - _context.TodoItems.RemoveRange(_context.TodoItems); + // Arrange + _context.RemoveRange(_context.TodoItems); + _context.RemoveRange(_context.TodoItemCollections); + _context.RemoveRange(_context.People); // ensure all people have todo-items + _context.SaveChanges(); var person = _personFaker.Generate(); var todoItem = _todoItemFaker.Generate(); todoItem.Owner = person; @@ -166,21 +177,24 @@ public async Task GET_Included_DoesNot_Duplicate_Records_ForMultipleRelationship var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - var data = documents.Data; - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotEmpty(documents.Included); Assert.Single(documents.Included); + + server.Dispose(); + request.Dispose(); + response.Dispose(); } [Fact] public async Task GET_Included_DoesNot_Duplicate_Records_If_HasOne_Exists_Twice() { - // arrange + // Arrange _context.TodoItemCollections.RemoveRange(_context.TodoItemCollections); _context.People.RemoveRange(_context.People); // ensure all people have todo-items _context.TodoItems.RemoveRange(_context.TodoItems); @@ -202,21 +216,24 @@ public async Task GET_Included_DoesNot_Duplicate_Records_If_HasOne_Exists_Twice( var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); - var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - var data = documents.Data; + var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotEmpty(documents.Included); Assert.Single(documents.Included); + + server.Dispose(); + request.Dispose(); + response.Dispose(); } [Fact] - public async Task GET_ById_Included_Contains_SideloadedData_ForOneToMany() + public async Task GET_ById_Included_Contains_SideloadeData_ForOneToMany() { - // arrange + // Arrange const int numberOfTodoItems = 5; var person = _personFaker.Generate(); for (var i = 0; i < numberOfTodoItems; i++) @@ -238,21 +255,25 @@ public async Task GET_ById_Included_Contains_SideloadedData_ForOneToMany() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); var responseString = await response.Content.ReadAsStringAsync(); var document = JsonConvert.DeserializeObject(responseString); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotEmpty(document.Included); Assert.Equal(numberOfTodoItems, document.Included.Count); + + server.Dispose(); + request.Dispose(); + response.Dispose(); } [Fact] public async Task Can_Include_MultipleRelationships() { - // arrange + // Arrange var person = _personFaker.Generate(); var todoItemCollection = _todoItemCollectionFaker.Generate(); todoItemCollection.Owner = person; @@ -278,21 +299,25 @@ public async Task Can_Include_MultipleRelationships() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); var responseString = await response.Content.ReadAsStringAsync(); var document = JsonConvert.DeserializeObject(responseString); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotEmpty(document.Included); Assert.Equal(numberOfTodoItems + 1, document.Included.Count); + + server.Dispose(); + request.Dispose(); + response.Dispose(); } [Fact] public async Task Request_ToIncludeUnknownRelationship_Returns_400() { - // arrange + // Arrange var person = _context.People.First(); var builder = new WebHostBuilder() @@ -306,17 +331,21 @@ public async Task Request_ToIncludeUnknownRelationship_Returns_400() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); - // assert + // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + server.Dispose(); + request.Dispose(); + response.Dispose(); } [Fact] public async Task Request_ToIncludeDeeplyNestedRelationships_Returns_400() { - // arrange + // Arrange var person = _context.People.First(); var builder = new WebHostBuilder() @@ -330,17 +359,21 @@ public async Task Request_ToIncludeDeeplyNestedRelationships_Returns_400() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); - // assert + // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + server.Dispose(); + request.Dispose(); + response.Dispose(); } [Fact] public async Task Request_ToIncludeRelationshipMarkedCanIncludeFalse_Returns_400() { - // arrange + // Arrange var person = _context.People.First(); var builder = new WebHostBuilder() @@ -354,17 +387,21 @@ public async Task Request_ToIncludeRelationshipMarkedCanIncludeFalse_Returns_400 var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); - // assert + // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + server.Dispose(); + request.Dispose(); + response.Dispose(); } [Fact] public async Task Can_Ignore_Null_Parent_In_Nested_Include() { - // arrange + // Arrange var todoItem = _todoItemFaker.Generate(); todoItem.Owner = _personFaker.Generate(); todoItem.CreatedDate = DateTime.Now; @@ -388,28 +425,32 @@ public async Task Can_Ignore_Null_Parent_In_Nested_Include() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); var responseString = await response.Content.ReadAsStringAsync(); - var documents = JsonConvert.DeserializeObject(responseString); + var documents = JsonConvert.DeserializeObject(responseString); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Single(documents.Included); - var ownerValueNull = documents.Data + var ownerValueNull = documents.ManyData .First(i => i.Id == todoItemWithNullOwner.StringId) .Relationships.First(i => i.Key == "owner") .Value.SingleData; Assert.Null(ownerValueNull); - var ownerValue = documents.Data + var ownerValue = documents.ManyData .First(i => i.Id == todoItem.StringId) .Relationships.First(i => i.Key == "owner") .Value.SingleData; Assert.NotNull(ownerValue); + + server.Dispose(); + request.Dispose(); + response.Dispose(); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs index 2b6b1e251b..b1e4b33017 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs @@ -7,7 +7,6 @@ using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; -using JsonApiDotNetCoreExampleTests.Startups; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Newtonsoft.Json; @@ -18,9 +17,9 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec.DocumentTests [Collection("WebHostCollection")] public class Meta { - private TestFixture _fixture; + private TestFixture _fixture; private AppDbContext _context; - public Meta(TestFixture fixture) + public Meta(TestFixture fixture) { _fixture = fixture; _context = fixture.GetService(); @@ -29,7 +28,7 @@ public Meta(TestFixture fixture) [Fact] public async Task Total_Record_Count_Included() { - // arrange + // Arrange _context.TodoItems.RemoveRange(_context.TodoItems); _context.TodoItems.Add(new TodoItem()); _context.SaveChanges(); @@ -44,12 +43,12 @@ public async Task Total_Record_Count_Included() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); var responseBody = await response.Content.ReadAsStringAsync(); - var documents = JsonConvert.DeserializeObject(responseBody); + var documents = JsonConvert.DeserializeObject(responseBody); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(documents.Meta); Assert.Equal((long)expectedCount, (long)documents.Meta["total-records"]); @@ -58,7 +57,7 @@ public async Task Total_Record_Count_Included() [Fact] public async Task Total_Record_Count_Included_When_None() { - // arrange + // Arrange _context.TodoItems.RemoveRange(_context.TodoItems); _context.SaveChanges(); var builder = new WebHostBuilder() @@ -71,12 +70,12 @@ public async Task Total_Record_Count_Included_When_None() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); var responseBody = await response.Content.ReadAsStringAsync(); - var documents = JsonConvert.DeserializeObject(responseBody); + var documents = JsonConvert.DeserializeObject(responseBody); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(documents.Meta); Assert.Equal(0, (long)documents.Meta["total-records"]); @@ -85,7 +84,7 @@ public async Task Total_Record_Count_Included_When_None() [Fact] public async Task Total_Record_Count_Not_Included_In_POST_Response() { - // arrange + // Arrange _context.TodoItems.RemoveRange(_context.TodoItems); _context.SaveChanges(); var builder = new WebHostBuilder() @@ -112,12 +111,12 @@ public async Task Total_Record_Count_Not_Included_In_POST_Response() request.Content = new StringContent(JsonConvert.SerializeObject(content)); request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); - // act + // Act var response = await client.SendAsync(request); var responseBody = await response.Content.ReadAsStringAsync(); var documents = JsonConvert.DeserializeObject(responseBody); - // assert + // Assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); Assert.False(documents.Meta.ContainsKey("total-records")); } @@ -125,7 +124,7 @@ public async Task Total_Record_Count_Not_Included_In_POST_Response() [Fact] public async Task Total_Record_Count_Not_Included_In_PATCH_Response() { - // arrange + // Arrange _context.TodoItems.RemoveRange(_context.TodoItems); TodoItem todoItem = new TodoItem(); _context.TodoItems.Add(todoItem); @@ -155,12 +154,12 @@ public async Task Total_Record_Count_Not_Included_In_PATCH_Response() request.Content = new StringContent(JsonConvert.SerializeObject(content)); request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); - // act + // Act var response = await client.SendAsync(request); var responseBody = await response.Content.ReadAsStringAsync(); var documents = JsonConvert.DeserializeObject(responseBody); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.False(documents.Meta.ContainsKey("total-records")); } @@ -168,11 +167,9 @@ public async Task Total_Record_Count_Not_Included_In_PATCH_Response() [Fact] public async Task EntityThatImplements_IHasMeta_Contains_MetaData() { - // arrange - var person = new Person(); - var expectedMeta = person.GetMeta(null); + // Arrange var builder = new WebHostBuilder() - .UseStartup(); + .UseStartup(); var httpMethod = new HttpMethod("GET"); var route = $"/api/v1/people"; @@ -180,12 +177,13 @@ public async Task EntityThatImplements_IHasMeta_Contains_MetaData() var server = new TestServer(builder); var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); + var expectedMeta = (_fixture.GetService>() as IHasMeta).GetMeta(); - // act + // Act var response = await client.SendAsync(request); - var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(documents.Meta); Assert.NotNull(expectedMeta); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs index e50032fce1..a15c92a38d 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs @@ -20,7 +20,7 @@ public class PagingTests private readonly AppDbContext _context; private readonly Faker _todoItemFaker; - public PagingTests(TestFixture fixture) + public PagingTests(TestFixture fixture) { _context = fixture.GetService(); _todoItemFaker = new Faker() @@ -32,7 +32,7 @@ public PagingTests(TestFixture fixture) [Fact] public async Task Server_IncludesPagination_Links() { - // arrange + // Arrange var pageSize = 5; const int minimumNumberOfRecords = 11; _context.TodoItems.RemoveRange(_context.TodoItems); @@ -55,12 +55,12 @@ public async Task Server_IncludesPagination_Links() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); - var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); var links = documents.Links; - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotEmpty(links.First); Assert.NotEmpty(links.Next); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Relationships.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Relationships.cs index 6c4bf56839..50c0848956 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Relationships.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Relationships.cs @@ -17,11 +17,11 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec.DocumentTests [Collection("WebHostCollection")] public class Relationships { - private TestFixture _fixture; + private TestFixture _fixture; private AppDbContext _context; private Faker _todoItemFaker; - public Relationships(TestFixture fixture) + public Relationships(TestFixture fixture) { _fixture = fixture; _context = fixture.GetService(); @@ -34,7 +34,7 @@ public Relationships(TestFixture fixture) [Fact] public async Task Correct_RelationshipObjects_For_ManyToOne_Relationships() { - // arrange + // Arrange var builder = new WebHostBuilder() .UseStartup(); @@ -49,14 +49,14 @@ public async Task Correct_RelationshipObjects_For_ManyToOne_Relationships() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); var document = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - var data = document.Data; + var data = document.SingleData; var expectedOwnerSelfLink = $"http://localhost/api/v1/todo-items/{data.Id}/relationships/owner"; var expectedOwnerRelatedLink = $"http://localhost/api/v1/todo-items/{data.Id}/owner"; - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(expectedOwnerSelfLink, data.Relationships["owner"].Links.Self); Assert.Equal(expectedOwnerRelatedLink, data.Relationships["owner"].Links.Related); @@ -65,7 +65,7 @@ public async Task Correct_RelationshipObjects_For_ManyToOne_Relationships() [Fact] public async Task Correct_RelationshipObjects_For_ManyToOne_Relationships_ById() { - // arrange + // Arrange var builder = new WebHostBuilder() .UseStartup(); @@ -80,14 +80,14 @@ public async Task Correct_RelationshipObjects_For_ManyToOne_Relationships_ById() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); var responseString = await response.Content.ReadAsStringAsync(); - var data = JsonConvert.DeserializeObject(responseString).Data; + var data = JsonConvert.DeserializeObject(responseString).SingleData; var expectedOwnerSelfLink = $"http://localhost/api/v1/todo-items/{todoItem.Id}/relationships/owner"; var expectedOwnerRelatedLink = $"http://localhost/api/v1/todo-items/{todoItem.Id}/owner"; - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(expectedOwnerSelfLink, data.Relationships["owner"].Links?.Self); Assert.Equal(expectedOwnerRelatedLink, data.Relationships["owner"].Links.Related); @@ -96,7 +96,7 @@ public async Task Correct_RelationshipObjects_For_ManyToOne_Relationships_ById() [Fact] public async Task Correct_RelationshipObjects_For_OneToMany_Relationships() { - // arrange + // Arrange var builder = new WebHostBuilder() .UseStartup(); @@ -107,14 +107,14 @@ public async Task Correct_RelationshipObjects_For_OneToMany_Relationships() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); - var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - var data = documents.Data[0]; + var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + var data = documents.ManyData.First(); var expectedOwnerSelfLink = $"http://localhost/api/v1/people/{data.Id}/relationships/todo-items"; var expectedOwnerRelatedLink = $"http://localhost/api/v1/people/{data.Id}/todo-items"; - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(expectedOwnerSelfLink, data.Relationships["todo-items"].Links.Self); Assert.Equal(expectedOwnerRelatedLink, data.Relationships["todo-items"].Links.Related); @@ -123,8 +123,8 @@ public async Task Correct_RelationshipObjects_For_OneToMany_Relationships() [Fact] public async Task Correct_RelationshipObjects_For_OneToMany_Relationships_ById() { - // arrange - var personId = _context.People.Last().Id; + // Arrange + var personId = _context.People.AsEnumerable().Last().Id; var builder = new WebHostBuilder() .UseStartup(); @@ -136,14 +136,14 @@ public async Task Correct_RelationshipObjects_For_OneToMany_Relationships_ById() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); var responseString = await response.Content.ReadAsStringAsync(); - var data = JsonConvert.DeserializeObject(responseString).Data; + var data = JsonConvert.DeserializeObject(responseString).SingleData; var expectedOwnerSelfLink = $"http://localhost/api/v1/people/{personId}/relationships/todo-items"; var expectedOwnerRelatedLink = $"http://localhost/api/v1/people/{personId}/todo-items"; - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(expectedOwnerSelfLink, data.Relationships["todo-items"].Links?.Self); Assert.Equal(expectedOwnerRelatedLink, data.Relationships["todo-items"].Links.Related); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs new file mode 100644 index 0000000000..e287e1ae20 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs @@ -0,0 +1,80 @@ +using System; +using System.Linq.Expressions; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization.Client; +using JsonApiDotNetCoreExample; +using JsonApiDotNetCoreExample.Data; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec +{ + public class EndToEndTest + { + public static MediaTypeHeaderValue JsonApiContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + private HttpClient _client; + protected TestFixture _fixture; + protected readonly IResponseDeserializer _deserializer; + public EndToEndTest(TestFixture fixture) + { + _fixture = fixture; + _deserializer = GetDeserializer(); + } + + public AppDbContext PrepareTest() where TStartup : class + { + var builder = new WebHostBuilder().UseStartup(); + var server = new TestServer(builder); + _client = server.CreateClient(); + + var dbContext = GetDbContext(); + dbContext.RemoveRange(dbContext.TodoItems); + dbContext.RemoveRange(dbContext.TodoItemCollections); + dbContext.RemoveRange(dbContext.PersonRoles); + dbContext.RemoveRange(dbContext.People); + dbContext.SaveChanges(); + return dbContext; + } + + public AppDbContext GetDbContext() + { + return _fixture.GetService(); + } + + public async Task<(string, HttpResponseMessage)> SendRequest(string method, string route, string content) + { + var request = new HttpRequestMessage(new HttpMethod(method), route); + request.Content = new StringContent(content); + request.Content.Headers.ContentType = JsonApiContentType; + var response = await _client.SendAsync(request); + var body = await response.Content?.ReadAsStringAsync(); + return (body, response); + } + + public Task<(string, HttpResponseMessage)> Post(string route, string content) + { + return SendRequest("POST", route, content); + } + + public IRequestSerializer GetSerializer(Expression> attributes = null, Expression> relationships = null) where TResource : class, IIdentifiable + { + return _fixture.GetSerializer(attributes, relationships); + } + + public IResponseDeserializer GetDeserializer() + { + return _fixture.GetDeserializer(); + } + + protected void AssertEqualStatusCode(HttpStatusCode expected, HttpResponseMessage response) + { + Assert.True(expected == response.StatusCode, $"Got {response.StatusCode} status code with payload instead of {expected}. Payload: {response.Content.ReadAsStringAsync().Result}"); + } + } + +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs index 9c8d5f8214..77be87e23a 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; -using System.Net; +using System.Net; using System.Net.Http; using System.Threading.Tasks; using Bogus; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -20,15 +17,13 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec [Collection("WebHostCollection")] public class FetchingDataTests { - private TestFixture _fixture; - private IJsonApiContext _jsonApiContext; + private TestFixture _fixture; private Faker _todoItemFaker; private Faker _personFaker; - public FetchingDataTests(TestFixture fixture) + public FetchingDataTests(TestFixture fixture) { _fixture = fixture; - _jsonApiContext = fixture.GetService(); _todoItemFaker = new Faker() .RuleFor(t => t.Description, f => f.Lorem.Sentence()) .RuleFor(t => t.Ordinal, f => f.Random.Number()) @@ -41,7 +36,7 @@ public FetchingDataTests(TestFixture fixture) [Fact] public async Task Request_ForEmptyCollection_Returns_EmptyDataCollection() { - // arrange + // Arrange var context = _fixture.GetService(); context.TodoItems.RemoveRange(context.TodoItems); await context.SaveChangesAsync(); @@ -53,30 +48,26 @@ public async Task Request_ForEmptyCollection_Returns_EmptyDataCollection() var server = new TestServer(builder); var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - var expectedBody = JsonConvert.SerializeObject(new - { - data = new List(), - meta = new Dictionary { { "total-records", 0 } } - }); - // act + // Act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService().DeserializeList(body); + var result = _fixture.GetDeserializer().DeserializeList(body); + var items = result.Data; + var meta = result.Meta; - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal("application/vnd.api+json", response.Content.Headers.ContentType.ToString()); - Assert.Empty(deserializedBody); - Assert.Equal(expectedBody, body); - + Assert.Empty(items); + Assert.Equal(0, int.Parse(meta["total-records"].ToString())); context.Dispose(); } [Fact] public async Task Included_Records_Contain_Relationship_Links() { - // arrange + // Arrange var context = _fixture.GetService(); var todoItem = _todoItemFaker.Generate(); var person = _personFaker.Generate(); @@ -92,12 +83,12 @@ public async Task Included_Records_Contain_Relationship_Links() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); var deserializedBody = JsonConvert.DeserializeObject(body); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(person.StringId, deserializedBody.Included[0].Id); Assert.NotNull(deserializedBody.Included[0].Relationships); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs index 9c9ea29ccb..0380d13487 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs @@ -2,7 +2,6 @@ using System.Net.Http; using System.Threading.Tasks; using Bogus; -using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -15,14 +14,12 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec [Collection("WebHostCollection")] public class FetchingRelationshipsTests { - private TestFixture _fixture; - private IJsonApiContext _jsonApiContext; + private TestFixture _fixture; private Faker _todoItemFaker; - public FetchingRelationshipsTests(TestFixture fixture) + public FetchingRelationshipsTests(TestFixture fixture) { _fixture = fixture; - _jsonApiContext = fixture.GetService(); _todoItemFaker = new Faker() .RuleFor(t => t.Description, f => f.Lorem.Sentence()) .RuleFor(t => t.Ordinal, f => f.Random.Number()) @@ -32,7 +29,7 @@ public FetchingRelationshipsTests(TestFixture fixture) [Fact] public async Task Request_UnsetRelationship_Returns_Null_DataObject() { - // arrange + // Arrange var context = _fixture.GetService(); var todoItem = _todoItemFaker.Generate(); context.TodoItems.Add(todoItem); @@ -46,13 +43,13 @@ public async Task Request_UnsetRelationship_Returns_Null_DataObject() var server = new TestServer(builder); var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - var expectedBody = "{\"data\":null}"; + var expectedBody = "{\"meta\":{\"copyright\":\"Copyright 2015 Example Corp.\",\"authors\":[\"Jared Nance\",\"Maurits Moeys\",\"Harro van der Kroft\"]},\"links\":{\"self\":\"http://localhost/api/v1/people\"},\"data\":null}"; - // act + // Act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal("application/vnd.api+json", response.Content.Headers.ContentType.ToString()); Assert.Equal(expectedBody, body); @@ -63,7 +60,7 @@ public async Task Request_UnsetRelationship_Returns_Null_DataObject() [Fact] public async Task Request_ForRelationshipLink_ThatDoesNotExist_Returns_404() { - // arrange + // Arrange var context = _fixture.GetService(); var todoItem = _todoItemFaker.Generate(); @@ -83,10 +80,10 @@ public async Task Request_ForRelationshipLink_ThatDoesNotExist_Returns_404() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); - // assert + // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); context.Dispose(); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PagingTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PagingTests.cs index 0667b51756..e404b605d4 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PagingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PagingTests.cs @@ -4,19 +4,28 @@ using System.Threading.Tasks; using Bogus; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCoreExampleTests.Helpers.Models; using Xunit; using Person = JsonApiDotNetCoreExample.Models.Person; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec { - public class PagingTests : TestFixture + [Collection("WebHostCollection")] + public class PagingTests : TestFixture { - private readonly Faker _todoItemFaker = new Faker() + private TestFixture _fixture; + private readonly Faker _todoItemFaker; + + public PagingTests(TestFixture fixture) + { + _fixture = fixture; + _todoItemFaker = new Faker() .RuleFor(t => t.Description, f => f.Lorem.Sentence()) .RuleFor(t => t.Ordinal, f => f.Random.Number()) .RuleFor(t => t.CreatedDate, f => f.Date.Past()); + } [Fact] public async Task Can_Paginate_TodoItems() @@ -42,7 +51,7 @@ public async Task Can_Paginate_TodoItems() Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = GetService().DeserializeList(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeList(body).Data; Assert.NotEmpty(deserializedBody); Assert.Equal(expectedEntitiesPerPage, deserializedBody.Count); @@ -73,7 +82,7 @@ public async Task Can_Paginate_TodoItems_From_Start() Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = GetService().DeserializeList(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeList(body).Data; var expectedTodoItems = new[] { todoItems[0], todoItems[1] }; Assert.Equal(expectedTodoItems, deserializedBody, new IdComparer()); @@ -87,27 +96,26 @@ public async Task Can_Paginate_TodoItems_From_End() var totalCount = expectedEntitiesPerPage * 2; var person = new Person(); var todoItems = _todoItemFaker.Generate(totalCount).ToList(); - - foreach (var todoItem in todoItems) - todoItem.Owner = person; + foreach (var ti in todoItems) + ti.Owner = person; Context.TodoItems.RemoveRange(Context.TodoItems); Context.TodoItems.AddRange(todoItems); Context.SaveChanges(); - var route = $"/api/v1/todo-items?page[size]={expectedEntitiesPerPage}&page[number]=-1"; // Act var response = await Client.GetAsync(route); // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = GetService().DeserializeList(body); - var expectedTodoItems = new[] { todoItems[totalCount - 2], todoItems[totalCount - 1] }; - Assert.Equal(expectedTodoItems, deserializedBody, new IdComparer()); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var deserializedBody = _fixture.GetDeserializer().DeserializeList(body).Data.Select(ti => ti.Id).ToArray(); + + var expectedTodoItems = new[] { todoItems[totalCount - 2].Id, todoItems[totalCount - 1].Id }; + for (int i = 0; i < expectedEntitiesPerPage-1 ; i++) + Assert.Contains(expectedTodoItems[i], deserializedBody); } private class IdComparer : IEqualityComparer diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/QueryParameters.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/QueryParameters.cs index 58b2323da5..4a7a8cfdcf 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/QueryParameters.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/QueryParameters.cs @@ -13,8 +13,8 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec [Collection("WebHostCollection")] public class QueryParameters { - private TestFixture _fixture; - public QueryParameters(TestFixture fixture) + private TestFixture _fixture; + public QueryParameters(TestFixture fixture) { _fixture = fixture; } @@ -22,7 +22,7 @@ public QueryParameters(TestFixture fixture) [Fact] public async Task Server_Returns_400_ForUnknownQueryParam() { - // arrange + // Arrange const string queryKey = "unknownKey"; const string queryValue = "value"; var builder = new WebHostBuilder() @@ -33,14 +33,15 @@ public async Task Server_Returns_400_ForUnknownQueryParam() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); - var body = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + var body = await response.Content.ReadAsStringAsync(); + var errorCollection = JsonConvert.DeserializeObject(body); - // assert + // Assert Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - Assert.Single(body.Errors); - Assert.Equal($"[{queryKey}, {queryValue}] is not a valid query.", body.Errors[0].Title); + Assert.Single(errorCollection.Errors); + Assert.Equal($"[{queryKey}, {queryValue}] is not a valid query.", errorCollection.Errors[0].Title); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs index 73f1ab524a..b39dcdb012 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs @@ -18,22 +18,26 @@ using StringExtensions = JsonApiDotNetCoreExampleTests.Helpers.Extensions.StringExtensions; using Person = JsonApiDotNetCoreExample.Models.Person; using System.Net; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Client; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCoreExampleTests.Helpers.Models; +using JsonApiDotNetCore.Services; +using JsonApiDotNetCore.Internal.Contracts; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec { [Collection("WebHostCollection")] public class SparseFieldSetTests { - private TestFixture _fixture; private readonly AppDbContext _dbContext; - private Faker _personFaker; - private Faker _todoItemFaker; + private readonly IResourceGraph _resourceGraph; + private readonly Faker _personFaker; + private readonly Faker _todoItemFaker; - public SparseFieldSetTests(TestFixture fixture) + public SparseFieldSetTests(TestFixture fixture) { - _fixture = fixture; _dbContext = fixture.GetService(); + _resourceGraph = fixture.GetService(); _personFaker = new Faker() .RuleFor(p => p.FirstName, f => f.Name.FirstName()) .RuleFor(p => p.LastName, f => f.Name.LastName()) @@ -41,14 +45,14 @@ public SparseFieldSetTests(TestFixture fixture) _todoItemFaker = new Faker() .RuleFor(t => t.Description, f => f.Lorem.Sentence()) - .RuleFor(t => t.Ordinal, f => f.Random.Number(1,10)) + .RuleFor(t => t.Ordinal, f => f.Random.Number(1, 10)) .RuleFor(t => t.CreatedDate, f => f.Date.Past()); } [Fact] public async Task Can_Select_Sparse_Fieldsets() { - // arrange + // Arrange var fields = new List { "Id", "Description", "CreatedDate", "AchievedDate" }; var todoItem = new TodoItem { @@ -63,27 +67,25 @@ public async Task Can_Select_Sparse_Fieldsets() FROM 'TodoItems' AS 't' WHERE 't'.'Id' = {todoItem.Id}"); - // act + // Act var query = _dbContext .TodoItems .Where(t => t.Id == todoItem.Id) - .Select(fields); + .Select(_resourceGraph.GetAttributes(e => new { e.Id, e.Description, e.CreatedDate, e.AchievedDate })); - var resultSql = StringExtensions.Normalize(query.ToSql()); var result = await query.FirstAsync(); - // assert + // Assert Assert.Equal(0, result.Ordinal); Assert.Equal(todoItem.Description, result.Description); Assert.Equal(todoItem.CreatedDate.ToString("G"), result.CreatedDate.ToString("G")); Assert.Equal(todoItem.AchievedDate.GetValueOrDefault().ToString("G"), result.AchievedDate.GetValueOrDefault().ToString("G")); - Assert.Equal(expectedSql, resultSql); } [Fact] public async Task Fields_Query_Selects_Sparse_Field_Sets() { - // arrange + // Arrange var todoItem = new TodoItem { Description = "description", @@ -96,34 +98,66 @@ public async Task Fields_Query_Selects_Sparse_Field_Sets() var builder = new WebHostBuilder() .UseStartup(); var httpMethod = new HttpMethod("GET"); - var server = new TestServer(builder); + using var server = new TestServer(builder); var client = server.CreateClient(); - var route = $"/api/v1/todo-items/{todoItem.Id}?fields[todo-items]=description,created-date"; + var route = $"/api/v1/todo-items/{todoItem.Id}?fields=description,created-date"; var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); var deserializeBody = JsonConvert.DeserializeObject(body); - // assert - Assert.Equal(todoItem.StringId, deserializeBody.Data.Id); - Assert.Equal(2, deserializeBody.Data.Attributes.Count); - Assert.Equal(todoItem.Description, deserializeBody.Data.Attributes["description"]); - Assert.Equal(todoItem.CreatedDate.ToString("G"), ((DateTime)deserializeBody.Data.Attributes["created-date"]).ToString("G")); + // Assert + Assert.Equal(todoItem.StringId, deserializeBody.SingleData.Id); + Assert.Equal(2, deserializeBody.SingleData.Attributes.Count); + Assert.Equal(todoItem.Description, deserializeBody.SingleData.Attributes["description"]); + Assert.Equal(todoItem.CreatedDate.ToString("G"), ((DateTime)deserializeBody.SingleData.Attributes["created-date"]).ToString("G")); + } + + [Fact] + public async Task Fields_Query_Selects_Sparse_Field_Sets_With_Type_As_Navigation() + { + // Arrange + var todoItem = new TodoItem + { + Description = "description", + Ordinal = 1, + CreatedDate = DateTime.Now + }; + _dbContext.TodoItems.Add(todoItem); + await _dbContext.SaveChangesAsync(); + + var builder = new WebHostBuilder() + .UseStartup(); + var httpMethod = new HttpMethod("GET"); + using var server = new TestServer(builder); + var client = server.CreateClient(); + var route = $"/api/v1/todo-items/{todoItem.Id}?fields[todo-items]=description,created-date"; + var request = new HttpRequestMessage(httpMethod, route); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Contains("relationships only", body); + } [Fact] public async Task Fields_Query_Selects_All_Fieldset_With_HasOne() { - // arrange + // Arrange + _dbContext.TodoItems.RemoveRange(_dbContext.TodoItems); + _dbContext.SaveChanges(); var owner = _personFaker.Generate(); - var ordinal = _dbContext.TodoItems.Count(); var todoItem = new TodoItem { Description = "s", - Ordinal = ordinal, + Ordinal = 123, CreatedDate = DateTime.Now, Owner = owner }; @@ -133,23 +167,23 @@ public async Task Fields_Query_Selects_All_Fieldset_With_HasOne() var builder = new WebHostBuilder() .UseStartup(); var httpMethod = new HttpMethod("GET"); - var server = new TestServer(builder); + using var server = new TestServer(builder); var client = server.CreateClient(); var route = $"/api/v1/todo-items?include=owner&fields[owner]=first-name,age"; var request = new HttpRequestMessage(httpMethod, route); - - // act + var resourceGraph = new ResourceGraphBuilder().AddResource().AddResource("todo-items").Build(); + var deserializer = new ResponseDeserializer(resourceGraph); + // Act var response = await client.SendAsync(request); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var deserializedTodoItems = _fixture - .GetService() - .DeserializeList(body); - foreach(var item in deserializedTodoItems.Where(i => i.Owner != null)) + var deserializedTodoItems = deserializer.DeserializeList(body).Data; + + foreach (var item in deserializedTodoItems.Where(i => i.Owner != null)) { Assert.Null(item.Owner.LastName); Assert.NotNull(item.Owner.FirstName); @@ -160,7 +194,7 @@ public async Task Fields_Query_Selects_All_Fieldset_With_HasOne() [Fact] public async Task Fields_Query_Selects_Fieldset_With_HasOne() { - // arrange + // Arrange var owner = _personFaker.Generate(); var todoItem = new TodoItem { @@ -175,23 +209,23 @@ public async Task Fields_Query_Selects_Fieldset_With_HasOne() var builder = new WebHostBuilder() .UseStartup(); var httpMethod = new HttpMethod("GET"); - var server = new TestServer(builder); + using var server = new TestServer(builder); var client = server.CreateClient(); var route = $"/api/v1/todo-items/{todoItem.Id}?include=owner&fields[owner]=first-name,age"; var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); - // assert + // Assert - check statusc ode Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); var deserializeBody = JsonConvert.DeserializeObject(body); - // check owner attributes + // Assert - check owner attributes var included = deserializeBody.Included.First(); - Assert.Equal(owner.StringId, included.Id); + Assert.Equal(owner.StringId, included.Id); Assert.Equal(owner.FirstName, included.Attributes["first-name"]); Assert.Equal((long)owner.Age, included.Attributes["age"]); Assert.DoesNotContain("last-name", included.Attributes.Keys); @@ -200,7 +234,7 @@ public async Task Fields_Query_Selects_Fieldset_With_HasOne() [Fact] public async Task Fields_Query_Selects_Fieldset_With_HasMany() { - // arrange + // Arrange var owner = _personFaker.Generate(); var todoItems = _todoItemFaker.Generate(2); @@ -212,16 +246,16 @@ public async Task Fields_Query_Selects_Fieldset_With_HasMany() var builder = new WebHostBuilder() .UseStartup(); var httpMethod = new HttpMethod("GET"); - var server = new TestServer(builder); + using var server = new TestServer(builder); var client = server.CreateClient(); var route = $"/api/v1/people/{owner.Id}?include=todo-items&fields[todo-items]=description"; var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); var deserializeBody = JsonConvert.DeserializeObject(body); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs index b565e48c56..9860f4e2cf 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs @@ -22,15 +22,16 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec [Collection("WebHostCollection")] public class UpdatingDataTests { - private TestFixture _fixture; + private TestFixture _fixture; private AppDbContext _context; private Faker _todoItemFaker; private Faker _personFaker; - public UpdatingDataTests(TestFixture fixture) + public UpdatingDataTests(TestFixture fixture) { _fixture = fixture; _context = fixture.GetService(); + _todoItemFaker = new Faker() .RuleFor(t => t.Description, f => f.Lorem.Sentence()) .RuleFor(t => t.Ordinal, f => f.Random.Number()) @@ -53,18 +54,8 @@ public async Task Response400IfUpdatingNotSettableAttribute() _context.TodoItems.Add(todoItem); _context.SaveChanges(); - var content = new - { - date = new - { - id = todoItem.Id, - type = "todo-items", - attributes = new - { - calculatedAttribute = "lol" - } - } - }; + var serializer = _fixture.GetSerializer(ti => new { ti.CalculatedValue }); + var content = serializer.Serialize(todoItem); var request = PrepareRequest("PATCH", $"/api/v1/todo-items/{todoItem.Id}", content); // Act @@ -79,28 +70,18 @@ public async Task Response400IfUpdatingNotSettableAttribute() public async Task Respond_404_If_EntityDoesNotExist() { // Arrange - var maxPersonId = _context.TodoItems.LastOrDefault()?.Id ?? 0; + var maxPersonId = _context.TodoItems.ToList().LastOrDefault()?.Id ?? 0; var todoItem = _todoItemFaker.Generate(); + todoItem.Id = maxPersonId + 100; + todoItem.CreatedDate = DateTime.Now; var builder = new WebHostBuilder() .UseStartup(); var server = new TestServer(builder); var client = server.CreateClient(); - var content = new - { - data = new - { - id = maxPersonId + 100, - type = "todo-items", - attributes = new - { - description = todoItem.Description, - ordinal = todoItem.Ordinal, - createdDate = DateTime.Now - } - } - }; + var serializer = _fixture.GetSerializer(ti => new { ti.Description, ti.Ordinal, ti.CreatedDate }); + var content = serializer.Serialize(todoItem); var request = PrepareRequest("PATCH", $"/api/v1/todo-items/{maxPersonId + 100}", content); // Act @@ -111,30 +92,19 @@ public async Task Respond_404_If_EntityDoesNotExist() } [Fact] - public async Task Respond_400_If_IdNotInAttributeList() + public async Task Respond_422_If_IdNotInAttributeList() { // Arrange - var maxPersonId = _context.TodoItems.LastOrDefault()?.Id ?? 0; + var maxPersonId = _context.TodoItems.ToList().LastOrDefault()?.Id ?? 0; var todoItem = _todoItemFaker.Generate(); + todoItem.CreatedDate = DateTime.Now; var builder = new WebHostBuilder() .UseStartup(); var server = new TestServer(builder); var client = server.CreateClient(); - - var content = new - { - data = new - { - type = "todo-items", - attributes = new - { - description = todoItem.Description, - ordinal = todoItem.Ordinal, - createdDate = DateTime.Now - } - } - }; + var serializer = _fixture.GetSerializer(ti => new { ti.Description, ti.Ordinal, ti.CreatedDate }); + var content = serializer.Serialize(todoItem); var request = PrepareRequest("PATCH", $"/api/v1/todo-items/{maxPersonId}", content); // Act @@ -145,11 +115,15 @@ public async Task Respond_400_If_IdNotInAttributeList() } - [Fact] public async Task Can_Patch_Entity() { - // arrange + // Arrange + _context.RemoveRange(_context.TodoItemCollections); + _context.RemoveRange(_context.TodoItems); + _context.RemoveRange(_context.People); + _context.SaveChanges(); + var todoItem = _todoItemFaker.Generate(); var person = _personFaker.Generate(); todoItem.Owner = person; @@ -157,25 +131,13 @@ public async Task Can_Patch_Entity() _context.SaveChanges(); var newTodoItem = _todoItemFaker.Generate(); - + newTodoItem.Id = todoItem.Id; var builder = new WebHostBuilder().UseStartup(); var server = new TestServer(builder); var client = server.CreateClient(); + var serializer = _fixture.GetSerializer(p => new { p.Description, p.Ordinal }); - var content = new - { - data = new - { - id = todoItem.Id, - type = "todo-items", - attributes = new - { - description = newTodoItem.Description, - ordinal = newTodoItem.Ordinal - } - } - }; - var request = PrepareRequest("PATCH", $"/api/v1/todo-items/{todoItem.Id}", content); + var request = PrepareRequest("PATCH", $"/api/v1/todo-items/{todoItem.Id}", serializer.Serialize(newTodoItem)); // Act var response = await client.SendAsync(request); @@ -186,20 +148,17 @@ public async Task Can_Patch_Entity() var document = JsonConvert.DeserializeObject(body); Assert.NotNull(document); Assert.NotNull(document.Data); - Assert.NotNull(document.Data.Attributes); - Assert.Equal(newTodoItem.Description, document.Data.Attributes["description"]); - Assert.Equal(newTodoItem.Ordinal, (long)document.Data.Attributes["ordinal"]); - Assert.True(document.Data.Relationships.ContainsKey("owner")); - Assert.NotNull(document.Data.Relationships["owner"].SingleData); - Assert.Equal(person.Id.ToString(), document.Data.Relationships["owner"].SingleData.Id); - Assert.Equal("people", document.Data.Relationships["owner"].SingleData.Type); + Assert.NotNull(document.SingleData.Attributes); + Assert.Equal(newTodoItem.Description, document.SingleData.Attributes["description"]); + Assert.Equal(newTodoItem.Ordinal, (long)document.SingleData.Attributes["ordinal"]); + Assert.True(document.SingleData.Relationships.ContainsKey("owner")); + Assert.Null(document.SingleData.Relationships["owner"].SingleData); // Assert -- database var updatedTodoItem = _context.TodoItems.AsNoTracking() .Include(t => t.Owner) .SingleOrDefault(t => t.Id == todoItem.Id); - - Assert.Equal(person.Id, updatedTodoItem.OwnerId); + Assert.Equal(person.Id, todoItem.OwnerId); Assert.Equal(newTodoItem.Description, updatedTodoItem.Description); Assert.Equal(newTodoItem.Ordinal, updatedTodoItem.Ordinal); } @@ -207,14 +166,7 @@ public async Task Can_Patch_Entity() [Fact] public async Task Patch_Entity_With_HasMany_Does_Not_Included_Relationships() { - /// @TODO: if we add a BeforeUpate resource hook to PersonDefinition - /// with database values enabled, this test will fail because todo-items - /// will be included in the person instance in the database-value loading. - /// This is then attached in the EF dbcontext, so when the query is executed and returned, - /// that entity will still have the relationship included even though the repo didn't include it. - - - // arrange + // Arrange var todoItem = _todoItemFaker.Generate(); var person = _personFaker.Generate(); todoItem.Owner = person; @@ -222,26 +174,13 @@ public async Task Patch_Entity_With_HasMany_Does_Not_Included_Relationships() _context.SaveChanges(); var newPerson = _personFaker.Generate(); - + newPerson.Id = person.Id; var builder = new WebHostBuilder().UseStartup(); var server = new TestServer(builder); var client = server.CreateClient(); + var serializer = _fixture.GetSerializer(p => new { p.LastName, p.FirstName }); - var content = new - { - data = new - { - type = "people", - id = person.Id, - - attributes = new Dictionary - { - { "last-name", newPerson.LastName }, - { "first-name", newPerson.FirstName}, - } - } - }; - var request = PrepareRequest("PATCH", $"/api/v1/people/{person.Id}", content); + var request = PrepareRequest("PATCH", $"/api/v1/people/{person.Id}", serializer.Serialize(newPerson)); // Act var response = await client.SendAsync(request); @@ -250,57 +189,33 @@ public async Task Patch_Entity_With_HasMany_Does_Not_Included_Relationships() Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); var document = JsonConvert.DeserializeObject(body); - Console.WriteLine(body); Assert.NotNull(document); Assert.NotNull(document.Data); - Assert.NotNull(document.Data.Attributes); - Assert.Equal(newPerson.LastName, document.Data.Attributes["last-name"]); - Assert.Equal(newPerson.FirstName, document.Data.Attributes["first-name"]); - Assert.True(document.Data.Relationships.ContainsKey("todo-items")); - Assert.Null(document.Data.Relationships["todo-items"].ManyData); - Assert.Null(document.Data.Relationships["todo-items"].SingleData); + Assert.NotNull(document.SingleData.Attributes); + Assert.Equal(newPerson.LastName, document.SingleData.Attributes["last-name"]); + Assert.Equal(newPerson.FirstName, document.SingleData.Attributes["first-name"]); + Assert.True(document.SingleData.Relationships.ContainsKey("todo-items")); + Assert.Null(document.SingleData.Relationships["todo-items"].Data); } [Fact] public async Task Can_Patch_Entity_And_HasOne_Relationships() { - // arrange + // Arrange var todoItem = _todoItemFaker.Generate(); + todoItem.CreatedDate = DateTime.Now; var person = _personFaker.Generate(); _context.TodoItems.Add(todoItem); _context.People.Add(person); _context.SaveChanges(); + todoItem.Owner = person; var builder = new WebHostBuilder() .UseStartup(); var server = new TestServer(builder); var client = server.CreateClient(); - - var content = new - { - data = new - { - type = "todo-items", - id = todoItem.Id, - attributes = new - { - description = todoItem.Description, - ordinal = todoItem.Ordinal, - createdDate = DateTime.Now - }, - relationships = new - { - owner = new - { - data = new - { - type = "people", - id = person.Id.ToString() - } - } - } - } - }; + var serializer = _fixture.GetSerializer(ti => new { ti.Description, ti.Ordinal, ti.CreatedDate }, ti => new { ti.Owner }); + var content = serializer.Serialize(todoItem); var request = PrepareRequest("PATCH", $"/api/v1/todo-items/{todoItem.Id}", content); // Act @@ -314,12 +229,12 @@ public async Task Can_Patch_Entity_And_HasOne_Relationships() Assert.Equal(person.Id, updatedTodoItem.OwnerId); } - private HttpRequestMessage PrepareRequest(string method, string route, object content) + private HttpRequestMessage PrepareRequest(string method, string route, string content) { var httpMethod = new HttpMethod(method); var request = new HttpRequestMessage(httpMethod, route); - request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content = new StringContent(content); request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); return request; } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs index 714b59d0e3..6dd6744210 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using Bogus; +using JsonApiDotNetCore.Models; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -12,6 +14,7 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Xunit; using Person = JsonApiDotNetCoreExample.Models.Person; @@ -20,12 +23,12 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec [Collection("WebHostCollection")] public class UpdatingRelationshipsTests { - private TestFixture _fixture; + private TestFixture _fixture; private AppDbContext _context; private Bogus.Faker _personFaker; private Faker _todoItemFaker; - public UpdatingRelationshipsTests(TestFixture fixture) + public UpdatingRelationshipsTests(TestFixture fixture) { _fixture = fixture; _context = fixture.GetService(); @@ -228,7 +231,7 @@ public async Task Can_Update_Both_Cyclic_ToOne_And_ToMany_Relationship_By_Patchi [Fact] public async Task Can_Update_ToMany_Relationship_By_Patching_Resource() { - // arrange + // Arrange var todoCollection = new TodoItemCollection(); todoCollection.TodoItems = new List(); var person = _personFaker.Generate(); @@ -302,7 +305,7 @@ public async Task Can_Update_ToMany_Relationship_By_Patching_Resource_When_Targe // in business logic in controllers. In this case, // this user may not be reattached to the db context in the repository. - // arrange + // Arrange var todoCollection = new TodoItemCollection(); todoCollection.TodoItems = new List(); var person = _personFaker.Generate(); @@ -375,7 +378,7 @@ public async Task Can_Update_ToMany_Relationship_By_Patching_Resource_When_Targe [Fact] public async Task Can_Update_ToMany_Relationship_By_Patching_Resource_With_Overlap() { - // arrange + // Arrange var todoCollection = new TodoItemCollection(); todoCollection.TodoItems = new List(); var person = _personFaker.Generate(); @@ -441,7 +444,7 @@ public async Task Can_Update_ToMany_Relationship_By_Patching_Resource_With_Overl [Fact] public async Task Can_Update_ToMany_Relationship_ThroughLink() { - // arrange + // Arrange var person = _personFaker.Generate(); _context.People.Add(person); @@ -487,7 +490,7 @@ public async Task Can_Update_ToMany_Relationship_ThroughLink() [Fact] public async Task Can_Update_ToOne_Relationship_ThroughLink() { - // arrange + // Arrange var person = _personFaker.Generate(); _context.People.Add(person); @@ -502,20 +505,14 @@ public async Task Can_Update_ToOne_Relationship_ThroughLink() var server = new TestServer(builder); var client = server.CreateClient(); - var content = new - { - data = new - { - type = "person", - id = $"{person.Id}" - } - }; + var serializer = _fixture.GetSerializer(p => new { }); + var content = serializer.Serialize(person); var httpMethod = new HttpMethod("PATCH"); var route = $"/api/v1/todo-items/{todoItem.Id}/relationships/owner"; var request = new HttpRequestMessage(httpMethod, route); - request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content = new StringContent(content); request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); // Act @@ -530,7 +527,7 @@ public async Task Can_Update_ToOne_Relationship_ThroughLink() [Fact] public async Task Can_Delete_ToOne_Relationship_By_Patching_Resource() { - // arrange + // Arrange var person = _personFaker.Generate(); var todoItem = _todoItemFaker.Generate(); todoItem.Owner = person; @@ -584,7 +581,7 @@ public async Task Can_Delete_ToOne_Relationship_By_Patching_Resource() [Fact] public async Task Can_Delete_ToMany_Relationship_By_Patching_Resource() { - // arrange + // Arrange var person = _personFaker.Generate(); var todoItem = _todoItemFaker.Generate(); person.TodoItems = new List() { todoItem }; @@ -641,7 +638,7 @@ public async Task Can_Delete_ToMany_Relationship_By_Patching_Resource() [Fact] public async Task Can_Delete_Relationship_By_Patching_Relationship() { - // arrange + // Arrange var person = _personFaker.Generate(); var todoItem = _todoItemFaker.Generate(); todoItem.Owner = person; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs index 6d6de345a0..a24bf58208 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs @@ -1,12 +1,16 @@ using System; using System.Net.Http; -using JsonApiDotNetCore.Serialization; using JsonApiDotNetCoreExample.Data; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; -using JsonApiDotNetCore.Services; using JsonApiDotNetCore.Data; using Microsoft.EntityFrameworkCore; +using JsonApiDotNetCore.Serialization.Client; +using System.Linq.Expressions; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCoreExampleTests.Helpers.Models; +using JsonApiDotNetCoreExample.Models; namespace JsonApiDotNetCoreExampleTests.Acceptance { @@ -14,25 +18,48 @@ public class TestFixture : IDisposable where TStartup : class { private readonly TestServer _server; private IServiceProvider _services; - public TestFixture() { var builder = new WebHostBuilder() .UseStartup(); - _server = new TestServer(builder); _services = _server.Host.Services; Client = _server.CreateClient(); Context = GetService().GetContext() as AppDbContext; - DeSerializer = GetService(); - JsonApiContext = GetService(); } public HttpClient Client { get; set; } public AppDbContext Context { get; private set; } - public IJsonApiDeSerializer DeSerializer { get; private set; } - public IJsonApiContext JsonApiContext { get; private set; } + public IRequestSerializer GetSerializer(Expression> attributes = null, Expression> relationships = null) where TResource : class, IIdentifiable + { + var serializer = GetService(); + if (attributes != null) + { + serializer.SetAttributesToSerialize(attributes); + } + if (relationships != null) + { + serializer.SetRelationshipsToSerialize(relationships); + } + return serializer; + } + public IResponseDeserializer GetDeserializer() + { + var resourceGraph = new ResourceGraphBuilder() + .AddResource() + .AddResource
() + .AddResource() + .AddResource() + .AddResource() + .AddResource() + .AddResource() + .AddResource() + .AddResource("todo-items") + .AddResource().Build(); + return new ResponseDeserializer(resourceGraph); + } + public T GetService() => (T)_services.GetService(typeof(T)); public void ReloadDbContext() @@ -60,4 +87,4 @@ public void Dispose() Dispose(true); } } -} \ No newline at end of file +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs index 9989a7f22b..a69ae3a70a 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs @@ -6,11 +6,12 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using Bogus; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCoreExampleTests.Helpers.Models; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using Xunit; @@ -21,17 +22,15 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance [Collection("WebHostCollection")] public class TodoItemControllerTests { - private TestFixture _fixture; + private TestFixture _fixture; private AppDbContext _context; - private IJsonApiContext _jsonApiContext; private Faker _todoItemFaker; private Faker _personFaker; - public TodoItemControllerTests(TestFixture fixture) + public TodoItemControllerTests(TestFixture fixture) { _fixture = fixture; _context = fixture.GetService(); - _jsonApiContext = fixture.GetService(); _todoItemFaker = new Faker() .RuleFor(t => t.Description, f => f.Lorem.Sentence()) .RuleFor(t => t.Ordinal, f => f.Random.Number()) @@ -44,15 +43,22 @@ public TodoItemControllerTests(TestFixture fixture) } [Fact] - public async Task Can_Get_TodoItems() + public async Task Can_Get_TodoItems_Paginate_Check() { // Arrange - const int expectedEntitiesPerPage = 5; - var person = new Person(); - var todoItem = _todoItemFaker.Generate(); - todoItem.Owner = person; - _context.TodoItems.Add(todoItem); + _context.TodoItems.RemoveRange(_context.TodoItems.ToList()); _context.SaveChanges(); + int expectedEntitiesPerPage = _fixture.GetService().DefaultPageSize; + var person = new Person(); + var todoItems = _todoItemFaker.Generate(expectedEntitiesPerPage + 1); + + foreach (var todoItem in todoItems) + { + todoItem.Owner = person; + _context.TodoItems.Add(todoItem); + _context.SaveChanges(); + + } var httpMethod = new HttpMethod("GET"); var route = "/api/v1/todo-items"; @@ -61,12 +67,12 @@ public async Task Can_Get_TodoItems() // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService().DeserializeList(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeList(body).Data; // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotEmpty(deserializedBody); - Assert.True(deserializedBody.Count <= expectedEntitiesPerPage); + Assert.True(deserializedBody.Count <= expectedEntitiesPerPage, $"There are more items on the page than the default page size. {deserializedBody.Count} > {expectedEntitiesPerPage}"); } [Fact] @@ -84,7 +90,7 @@ public async Task Can_Filter_By_Resource_Id() // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService().DeserializeList(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeList(body).Data; // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -96,10 +102,10 @@ public async Task Can_Filter_By_Resource_Id() public async Task Can_Filter_By_Relationship_Id() { // Arrange - var person = new Person(); - var todoItem = _todoItemFaker.Generate(); - todoItem.Owner = person; - _context.TodoItems.Add(todoItem); + var person = new Person(); + var todoItems = _todoItemFaker.Generate(3).ToList(); + _context.TodoItems.AddRange(todoItems); + todoItems[0].Owner = person; _context.SaveChanges(); var httpMethod = new HttpMethod("GET"); @@ -109,12 +115,12 @@ public async Task Can_Filter_By_Relationship_Id() // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService().DeserializeList(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeList(body).Data; // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotEmpty(deserializedBody); - Assert.Contains(deserializedBody, (i) => i.Owner.Id == person.Id); + Assert.Contains(deserializedBody, (i) => i.Id == todoItems[0].Id); } [Fact] @@ -135,7 +141,7 @@ public async Task Can_Filter_TodoItems() // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService().DeserializeList(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeList(body).Data; // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -168,7 +174,7 @@ public async Task Can_Filter_TodoItems_Using_IsNotNull_Operator() Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var todoItems = _fixture.GetService().DeserializeList(body); + var todoItems = _fixture.GetDeserializer().DeserializeList(body).Data; // Assert Assert.NotEmpty(todoItems); @@ -185,6 +191,7 @@ public async Task Can_Filter_TodoItems_ByParent_Using_IsNotNull_Operator() var otherTodoItem = _todoItemFaker.Generate(); otherTodoItem.Assignee = null; + _context.RemoveRange(_context.TodoItems); _context.TodoItems.AddRange(new[] { todoItem, otherTodoItem }); _context.SaveChanges(); @@ -194,15 +201,13 @@ public async Task Can_Filter_TodoItems_ByParent_Using_IsNotNull_Operator() // Act var response = await _fixture.Client.SendAsync(request); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var todoItems = _fixture.GetService().DeserializeList(body); + var list = _fixture.GetDeserializer().DeserializeList(body).Data; // Assert - Assert.NotEmpty(todoItems); - Assert.All(todoItems, t => Assert.NotNull(t.Assignee)); + Assert.Equal(todoItem.Id, list.Single().Id); } [Fact] @@ -228,7 +233,7 @@ public async Task Can_Filter_TodoItems_Using_IsNull_Operator() Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var todoItems = _fixture.GetService().DeserializeList(body); + var todoItems = _fixture.GetDeserializer().DeserializeList(body).Data; // Assert Assert.NotEmpty(todoItems); @@ -258,7 +263,7 @@ public async Task Can_Filter_TodoItems_ByParent_Using_IsNull_Operator() Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var todoItems = _fixture.GetService().DeserializeList(body); + var todoItems = _fixture.GetDeserializer().DeserializeList(body).Data; // Assert Assert.NotEmpty(todoItems); @@ -282,7 +287,7 @@ public async Task Can_Filter_TodoItems_Using_Like_Operator() // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService().DeserializeList(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeList(body).Data; // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -317,7 +322,7 @@ public async Task Can_Sort_TodoItems_By_Ordinal_Ascending() // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService().DeserializeList(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeList(body).Data; // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -358,7 +363,7 @@ public async Task Can_Sort_TodoItems_By_Nested_Attribute_Ascending() // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService().DeserializeList(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeList(body).Data; Assert.NotEmpty(deserializedBody); long lastAge = 0; @@ -396,7 +401,7 @@ public async Task Can_Sort_TodoItems_By_Nested_Attribute_Descending() // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService().DeserializeList(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeList(body).Data; Assert.NotEmpty(deserializedBody); int maxAge = deserializedBody.Max(i => i.Owner.Age) + 1; @@ -432,7 +437,7 @@ public async Task Can_Sort_TodoItems_By_Ordinal_Descending() // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService().DeserializeList(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeList(body).Data; // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -463,7 +468,7 @@ public async Task Can_Get_TodoItem_ById() // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeSingle(body).Data; // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -494,7 +499,7 @@ public async Task Can_Get_TodoItem_WithOwner() // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeSingle(body).Data; Assert.Equal(person.Id, deserializedBody.Owner.Id); Assert.Equal(todoItem.Id, deserializedBody.Id); @@ -512,39 +517,17 @@ public async Task Can_Post_TodoItem() _context.People.Add(person); _context.SaveChanges(); + var serializer = _fixture.GetSerializer(e => new { e.Description, e.OffsetDate, e.Ordinal, e.CreatedDate }, e => new { e.Owner }); + var todoItem = _todoItemFaker.Generate(); var nowOffset = new DateTimeOffset(); - var content = new - { - data = new - { - type = "todo-items", - attributes = new Dictionary() - { - { "description", todoItem.Description }, - { "ordinal", todoItem.Ordinal }, - { "created-date", todoItem.CreatedDate }, - { "offset-date", nowOffset } - }, - relationships = new - { - owner = new - { - data = new - { - type = "people", - id = person.Id.ToString() - } - } - } - } - }; + todoItem.OffsetDate = nowOffset; var httpMethod = new HttpMethod("POST"); var route = $"/api/v1/todo-items"; var request = new HttpRequestMessage(httpMethod, route); - request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content = new StringContent(serializer.Serialize(todoItem)); request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); // Act @@ -553,7 +536,7 @@ public async Task Can_Post_TodoItem() // Assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeSingle(body).Data; Assert.Equal(HttpStatusCode.Created, response.StatusCode); Assert.Equal(todoItem.Description, deserializedBody.Description); Assert.Equal(todoItem.CreatedDate.ToString("G"), deserializedBody.CreatedDate.ToString("G")); @@ -620,7 +603,7 @@ public async Task Can_Post_TodoItem_With_Different_Owner_And_Assignee() Assert.Equal(HttpStatusCode.Created, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); var document = JsonConvert.DeserializeObject(body); - var resultId = int.Parse(document.Data.Id); + var resultId = int.Parse(document.SingleData.Id); // Assert -- database var todoItemResult = await _context.TodoItems.SingleAsync(t => t.Id == resultId); @@ -669,7 +652,7 @@ public async Task Can_Patch_TodoItem() // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeSingle(body).Data; // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -722,7 +705,7 @@ public async Task Can_Patch_TodoItemWithNullable() // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeSingle(body).Data; // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -774,7 +757,7 @@ public async Task Can_Patch_TodoItemWithNullValue() // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); + var deserializedBody = _fixture.GetDeserializer().DeserializeSingle(body).Data; // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/test/JsonApiDotNetCoreExampleTests/CamelCaseTestStartup.cs b/test/JsonApiDotNetCoreExampleTests/CamelCaseTestStartup.cs new file mode 100644 index 0000000000..f801c593a7 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/CamelCaseTestStartup.cs @@ -0,0 +1,25 @@ +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Graph; +using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreExample; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using System; +using UnitTests; + +namespace JsonApiDotNetCoreExampleTests +{ + public class CamelCaseTestStartup : Startup + { + public CamelCaseTestStartup(IWebHostEnvironment env) : base(env) + { } + + public override void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + base.ConfigureServices(services); + services.AddClientSerialization(); + services.AddScoped(); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/IQueryableExtensions.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/IQueryableExtensions.cs deleted file mode 100644 index 9298d93a05..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/IQueryableExtensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using Microsoft.EntityFrameworkCore.Internal; -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.Query.Internal; -using Microsoft.EntityFrameworkCore.Storage; -using Database = Microsoft.EntityFrameworkCore.Storage.Database; - -namespace JsonApiDotNetCoreExampleTests.Helpers.Extensions -{ - public static class IQueryableExtensions - { - private static readonly FieldInfo QueryCompilerField = typeof(EntityQueryProvider).GetTypeInfo().DeclaredFields.Single(x => x.Name == "_queryCompiler"); - - private static readonly TypeInfo QueryCompilerTypeInfo = typeof(QueryCompiler).GetTypeInfo(); - - private static readonly FieldInfo QueryModelGeneratorField = QueryCompilerTypeInfo.DeclaredFields.Single(x => x.Name == "_queryModelGenerator"); - - private static readonly FieldInfo DatabaseField = QueryCompilerTypeInfo.DeclaredFields.Single(x => x.Name == "_database"); - - private static readonly PropertyInfo DependenciesProperty = typeof(Database).GetTypeInfo().DeclaredProperties.Single(x => x.Name == "Dependencies"); - - public static string ToSql(this IQueryable queryable) - where TEntity : class - { - if (!(queryable is EntityQueryable) && !(queryable is InternalDbSet)) - throw new ArgumentException(); - - var queryCompiler = (IQueryCompiler)QueryCompilerField.GetValue(queryable.Provider); - var queryModelGenerator = (IQueryModelGenerator)QueryModelGeneratorField.GetValue(queryCompiler); - var queryModel = queryModelGenerator.ParseQuery(queryable.Expression); - var database = DatabaseField.GetValue(queryCompiler); - var queryCompilationContextFactory = ((DatabaseDependencies)DependenciesProperty.GetValue(database)).QueryCompilationContextFactory; - var queryCompilationContext = queryCompilationContextFactory.Create(false); - var modelVisitor = (RelationalQueryModelVisitor)queryCompilationContext.CreateQueryModelVisitor(); - modelVisitor.CreateQueryExecutor(queryModel); - return modelVisitor.Queries.Join(Environment.NewLine + Environment.NewLine); - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Models/TodoItemClient.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Models/TodoItemClient.cs new file mode 100644 index 0000000000..77e928358f --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Helpers/Models/TodoItemClient.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCoreExample.Models; + +namespace JsonApiDotNetCoreExampleTests.Helpers.Models +{ + /// + /// this "client" version of the is required because the + /// base property that is overridden here does not have a setter. For a model + /// defind on a json:api client, it would not make sense to have an exposed attribute + /// without a setter. + /// + public class TodoItemClient : TodoItem + { + [Attr("calculated-value")] + public new string CalculatedValue { get; set; } + } + + //[Resource("todo-collections")] + //public class TodoItemCollectionClient : TodoItemCollection + //{ + // [HasMany("todo-items")] + // public new List TodoItems { get; set; } + //} + + [Resource("todo-collections")] + public class TodoItemCollectionClient : Identifiable + { + [Attr("name")] + public string Name { get; set; } + public int OwnerId { get; set; } + + [HasMany("todo-items")] + public virtual List TodoItems { get; set; } + + [HasOne("owner")] + public virtual Person Owner { get; set; } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Services/IAuthorizationService.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Services/IAuthorizationService.cs deleted file mode 100644 index b994c7f8bd..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/Helpers/Services/IAuthorizationService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace JsonApiDotNetCoreExampleTests.Services -{ - public interface IAuthorizationService - { - int CurrentUserId { get; set; } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Services/MetaService.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Services/MetaService.cs deleted file mode 100644 index 91de8fda5e..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/Helpers/Services/MetaService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCoreExampleTests.Services -{ - public class MetaService : IRequestMeta - { - public Dictionary GetMeta() - { - return new Dictionary { - { "request-meta", "request-meta-value" } - }; - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/AuthorizedStartup.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/AuthorizedStartup.cs deleted file mode 100644 index 12a207fea8..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/AuthorizedStartup.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using JsonApiDotNetCoreExample.Data; -using Microsoft.EntityFrameworkCore; -using JsonApiDotNetCore.Extensions; -using System; -using JsonApiDotNetCoreExample; -using Moq; -using JsonApiDotNetCoreExampleTests.Services; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCoreExample.Models; -using JsonApiDotNetCoreExampleTests.Repositories; -using UnitTests; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCoreExampleTests.Startups -{ - public class AuthorizedStartup : Startup - { - public AuthorizedStartup(IHostingEnvironment env) - : base(env) - { } - - public override IServiceProvider ConfigureServices(IServiceCollection services) - { - var loggerFactory = new LoggerFactory(); - - loggerFactory.AddConsole(); - - services.AddSingleton(loggerFactory); - - services.AddDbContext(options => - { - options.UseNpgsql(GetDbConnectionString()); - }, ServiceLifetime.Transient); - - services.AddJsonApi(opt => - { - opt.Namespace = "api/v1"; - opt.DefaultPageSize = 5; - opt.IncludeTotalRecordCount = true; - }); - - // custom authorization implementation - var authServicMock = new Mock(); - authServicMock.SetupAllProperties(); - services.AddSingleton(authServicMock.Object); - services.AddScoped, AuthorizedTodoItemsRepository>(); - - services.AddScoped(); - - return services.BuildServiceProvider(); - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/ClientGeneratedIdsStartup.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/ClientGeneratedIdsStartup.cs deleted file mode 100644 index 32d8186802..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/ClientGeneratedIdsStartup.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using JsonApiDotNetCoreExample.Data; -using Microsoft.EntityFrameworkCore; -using JsonApiDotNetCore.Extensions; -using System; -using JsonApiDotNetCoreExample; - -namespace JsonApiDotNetCoreExampleTests.Startups -{ - public class ClientGeneratedIdsStartup : Startup - { - public ClientGeneratedIdsStartup(IHostingEnvironment env) - : base (env) - { } - - public override IServiceProvider ConfigureServices(IServiceCollection services) - { - var loggerFactory = new LoggerFactory(); - - loggerFactory.AddConsole(); - - services.AddSingleton(loggerFactory); - - services.AddDbContext(options => - { - options.UseNpgsql(GetDbConnectionString()); - }, ServiceLifetime.Transient); - - services.AddJsonApi(opt => - { - opt.Namespace = "api/v1"; - opt.DefaultPageSize = 5; - opt.IncludeTotalRecordCount = true; - opt.AllowClientGeneratedIds = true; - }); - - return services.BuildServiceProvider(); - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/MetaStartup.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/MetaStartup.cs deleted file mode 100644 index 6bc5a08016..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/MetaStartup.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using JsonApiDotNetCoreExample.Data; -using Microsoft.EntityFrameworkCore; -using JsonApiDotNetCore.Extensions; -using System; -using JsonApiDotNetCoreExample; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExampleTests.Services; - -namespace JsonApiDotNetCoreExampleTests.Startups -{ - public class MetaStartup : Startup - { - public MetaStartup(IHostingEnvironment env) - : base (env) - { } - - public override IServiceProvider ConfigureServices(IServiceCollection services) - { - var loggerFactory = new LoggerFactory(); - loggerFactory.AddConsole(LogLevel.Warning); - - services - .AddSingleton(loggerFactory) - .AddDbContext(options => - options.UseNpgsql(GetDbConnectionString()), ServiceLifetime.Transient) - .AddJsonApi(options => { - options.Namespace = "api/v1"; - options.DefaultPageSize = 5; - options.IncludeTotalRecordCount = true; - }) - .AddScoped(); - - return services.BuildServiceProvider(); - } - } -} diff --git a/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj b/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj index b4fcaf7ae0..7a099cdf48 100644 --- a/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj +++ b/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppVersion) false @@ -17,16 +17,23 @@ - + + + + + + + + - - + + diff --git a/test/JsonApiDotNetCoreExampleTests/TestStartup.cs b/test/JsonApiDotNetCoreExampleTests/TestStartup.cs index 886d6b3424..73cc900b51 100644 --- a/test/JsonApiDotNetCoreExampleTests/TestStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/TestStartup.cs @@ -1,22 +1,22 @@ +using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; -using System; using UnitTests; namespace JsonApiDotNetCoreExampleTests { public class TestStartup : Startup { - public TestStartup(IHostingEnvironment env) : base(env) + public TestStartup(IWebHostEnvironment env) : base(env) { } - public override IServiceProvider ConfigureServices(IServiceCollection services) + public override void ConfigureServices(IServiceCollection services) { base.ConfigureServices(services); + services.AddClientSerialization(); services.AddScoped(); - return services.BuildServiceProvider(); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/WebHostCollection.cs b/test/JsonApiDotNetCoreExampleTests/WebHostCollection.cs index 561d86bbb0..edf8bac897 100644 --- a/test/JsonApiDotNetCoreExampleTests/WebHostCollection.cs +++ b/test/JsonApiDotNetCoreExampleTests/WebHostCollection.cs @@ -1,10 +1,10 @@ +using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExampleTests.Acceptance; using Xunit; namespace JsonApiDotNetCoreExampleTests { [CollectionDefinition("WebHostCollection")] - public class WebHostCollection - : ICollectionFixture> + public class WebHostCollection : ICollectionFixture> { } } diff --git a/test/NoEntityFrameworkTests/Acceptance/Extensibility/NoEntityFrameworkTests.cs b/test/NoEntityFrameworkTests/Acceptance/Extensibility/NoEntityFrameworkTests.cs index 95e2f32142..06dd96a854 100644 --- a/test/NoEntityFrameworkTests/Acceptance/Extensibility/NoEntityFrameworkTests.cs +++ b/test/NoEntityFrameworkTests/Acceptance/Extensibility/NoEntityFrameworkTests.cs @@ -3,11 +3,12 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCoreExample.Models; -using JsonApiDotNetCoreExampleTests.Helpers.Extensions; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; using Newtonsoft.Json; using Xunit; +using Startup = NoEntityFrameworkExample.Startup; +using TodoItem = NoEntityFrameworkExample.Models.TodoItem; namespace NoEntityFrameworkTests.Acceptance.Extensibility { @@ -23,24 +24,23 @@ public NoEntityFrameworkTests(TestFixture fixture) [Fact] public async Task Can_Get_TodoItems() { - // arrange + // Arrange _fixture.Context.TodoItems.Add(new TodoItem()); _fixture.Context.SaveChanges(); var client = _fixture.Server.CreateClient(); var httpMethod = new HttpMethod("GET"); - var route = $"/api/v1/custom-todo-items"; + var route = $"/api/v1/todo-items"; var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() - .DeserializeList(responseBody); + var deserializedBody = _fixture.GetDeserializer().DeserializeList(responseBody).Data; - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(deserializedBody); Assert.NotEmpty(deserializedBody); @@ -49,7 +49,7 @@ public async Task Can_Get_TodoItems() [Fact] public async Task Can_Get_TodoItems_By_Id() { - // arrange + // Arrange var todoItem = new TodoItem(); _fixture.Context.TodoItems.Add(todoItem); _fixture.Context.SaveChanges(); @@ -57,17 +57,16 @@ public async Task Can_Get_TodoItems_By_Id() var client = _fixture.Server.CreateClient(); var httpMethod = new HttpMethod("GET"); - var route = $"/api/v1/custom-todo-items/{todoItem.Id}"; + var route = $"/api/v1/todo-items/{todoItem.Id}"; var request = new HttpRequestMessage(httpMethod, route); - // act + // Act var response = await client.SendAsync(request); var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)_fixture.Server.GetService() - .Deserialize(responseBody); + var deserializedBody = _fixture.GetDeserializer().DeserializeSingle(responseBody).Data; - // assert + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(deserializedBody); Assert.Equal(todoItem.Id, deserializedBody.Id); @@ -76,16 +75,15 @@ public async Task Can_Get_TodoItems_By_Id() [Fact] public async Task Can_Create_TodoItems() { - // arrange + // Arrange var description = Guid.NewGuid().ToString(); - var client = _fixture.Server.CreateClient(); var httpMethod = new HttpMethod("POST"); - var route = $"/api/v1/custom-todo-items/"; + var route = $"/api/v1/todo-items/"; var content = new { data = new { - type = "custom-todo-items", + type = "todo-items", attributes = new { description, @@ -98,13 +96,17 @@ public async Task Can_Create_TodoItems() request.Content = new StringContent(JsonConvert.SerializeObject(content)); request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); - // act + var builder = new WebHostBuilder() + .UseStartup(); + var server = new TestServer(builder); + var client = server.CreateClient(); + + // Act var response = await client.SendAsync(request); var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)_fixture.Server.GetService() - .Deserialize(responseBody); + var deserializedBody = _fixture.GetDeserializer().DeserializeSingle(responseBody).Data; - // assert + // Assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); Assert.NotNull(deserializedBody); Assert.Equal(description, deserializedBody.Description); diff --git a/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj b/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj index 6e2fe18abd..a53c1e90ce 100644 --- a/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj +++ b/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj @@ -14,18 +14,17 @@ - - + - + diff --git a/test/NoEntityFrameworkTests/TestFixture.cs b/test/NoEntityFrameworkTests/TestFixture.cs index 3a317e03cd..82836a8a06 100644 --- a/test/NoEntityFrameworkTests/TestFixture.cs +++ b/test/NoEntityFrameworkTests/TestFixture.cs @@ -1,8 +1,13 @@ -using JsonApiDotNetCoreExample.Data; -using JsonApiDotNetCoreExampleTests.Helpers.Extensions; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization.Client; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; +using NoEntityFrameworkExample.Data; +using NoEntityFrameworkExample.Models; using System; +using System.Linq.Expressions; +using Startup = NoEntityFrameworkExample.Startup; namespace NoEntityFrameworkTests { @@ -10,14 +15,32 @@ public class TestFixture : IDisposable { public AppDbContext Context { get; private set; } public TestServer Server { get; private set; } - + private IServiceProvider _services; public TestFixture() { - var builder = new WebHostBuilder().UseStartup(); + var builder = new WebHostBuilder().UseStartup(); Server = new TestServer(builder); - Context = Server.GetService(); + Context = (AppDbContext)Server.Services.GetService(typeof(AppDbContext)); Context.Database.EnsureCreated(); + _services = Server.Host.Services; + } + + public IRequestSerializer GetSerializer(Expression> attributes = null, Expression> relationships = null) where TResource : class, IIdentifiable + { + var serializer = GetService(); + if (attributes != null) + serializer.SetAttributesToSerialize(attributes); + if (relationships != null) + serializer.SetRelationshipsToSerialize(relationships); + return serializer; } + public IResponseDeserializer GetDeserializer() + { + var resourceGraph = new ResourceGraphBuilder().AddResource("todo-items").Build(); + return new ResponseDeserializer(resourceGraph); + } + + public T GetService() => (T)_services.GetService(typeof(T)); public void Dispose() { diff --git a/test/NoEntityFrameworkTests/TestStartup.cs b/test/NoEntityFrameworkTests/TestStartup.cs index e925e69fd0..3f0cd1fc4c 100644 --- a/test/NoEntityFrameworkTests/TestStartup.cs +++ b/test/NoEntityFrameworkTests/TestStartup.cs @@ -9,14 +9,14 @@ namespace NoEntityFrameworkTests { public class TestStartup : Startup { - public TestStartup(IHostingEnvironment env) : base(env) + public TestStartup(IWebHostEnvironment env) : base(env) { } - public override IServiceProvider ConfigureServices(IServiceCollection services) + public override void ConfigureServices(IServiceCollection services) { base.ConfigureServices(services); services.AddScoped(); - return services.BuildServiceProvider(); + services.BuildServiceProvider(); } } } diff --git a/test/OperationsExampleTests/.gitignore b/test/OperationsExampleTests/.gitignore deleted file mode 100644 index 0ca27f04e1..0000000000 --- a/test/OperationsExampleTests/.gitignore +++ /dev/null @@ -1,234 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -build/ -bld/ -[Bb]in/ -[Oo]bj/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Microsoft Azure ApplicationInsights config file -ApplicationInsights.config - -# Windows Store app package directory -AppPackages/ -BundleArtifacts/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe - -# FAKE - F# Make -.fake/ diff --git a/test/OperationsExampleTests/Add/AddTests.cs b/test/OperationsExampleTests/Add/AddTests.cs deleted file mode 100644 index 0deaaa6a82..0000000000 --- a/test/OperationsExampleTests/Add/AddTests.cs +++ /dev/null @@ -1,294 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Bogus; -using JsonApiDotNetCore.Models.Operations; -using JsonApiDotNetCoreExample.Data; -using Microsoft.EntityFrameworkCore; -using OperationsExampleTests.Factories; -using Xunit; - -namespace OperationsExampleTests -{ - public class AddTests : Fixture - { - private readonly Faker _faker = new Faker(); - - [Fact] - public async Task Can_Create_Author() - { - // arrange - var context = GetService(); - var author = AuthorFactory.Get(); - var content = new - { - operations = new[] { - new { - op = "add", - data = new { - type = "authors", - attributes = new { - name = author.Name - } - } - } - } - }; - - // act - var (response, data) = await PatchAsync("api/bulk", content); - - // assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var id = data.Operations.Single().DataObject.Id; - var lastAuthor = await context.Authors.SingleAsync(a => a.StringId == id); - Assert.Equal(author.Name, lastAuthor.Name); - } - - [Fact] - public async Task Can_Create_Authors() - { - // arrange - var expectedCount = _faker.Random.Int(1, 10); - var context = GetService(); - var authors = AuthorFactory.Get(expectedCount); - var content = new - { - operations = new List() - }; - - for (int i = 0; i < expectedCount; i++) - { - content.operations.Add( - new - { - op = "add", - data = new - { - type = "authors", - attributes = new - { - name = authors[i].Name - } - } - } - ); - } - - // act - var (response, data) = await PatchAsync("api/bulk", content); - - // assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(expectedCount, data.Operations.Count); - - for (int i = 0; i < expectedCount; i++) - { - var dataObject = data.Operations[i].DataObject; - var author = context.Authors.Single(a => a.StringId == dataObject.Id); - Assert.Equal(authors[i].Name, author.Name); - } - } - - [Fact] - public async Task Can_Create_Article_With_Existing_Author() - { - // arrange - var context = GetService(); - var author = AuthorFactory.Get(); - var article = ArticleFactory.Get(); - - context.Authors.Add(author); - await context.SaveChangesAsync(); - - - //const string authorLocalId = "author-1"; - - var content = new - { - operations = new object[] { - new { - op = "add", - data = new { - type = "articles", - attributes = new { - name = article.Name - }, - relationships = new { - author = new { - data = new { - type = "authors", - id = author.Id - } - } - } - } - } - } - }; - - // act - var (response, data) = await PatchAsync("api/bulk", content); - - // assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Single(data.Operations); - - - var lastAuthor = await context.Authors - .Include(a => a.Articles) - .SingleAsync(a => a.Id == author.Id); - var articleOperationResult = data.Operations[0]; - - // author validation: sanity checks - Assert.NotNull(lastAuthor); - Assert.Equal(author.Name, lastAuthor.Name); - - //// article validation - Assert.Single(lastAuthor.Articles); - Assert.Equal(article.Name, lastAuthor.Articles[0].Name); - Assert.Equal(articleOperationResult.DataObject.Id, lastAuthor.Articles[0].StringId); - } - - [Fact] - public async Task Can_Create_Articles_With_Existing_Author() - { - - - // arrange - var context = GetService(); - var author = AuthorFactory.Get(); - context.Authors.Add(author); - await context.SaveChangesAsync(); - var expectedCount = _faker.Random.Int(1, 10); - var articles = ArticleFactory.Get(expectedCount); - - var content = new - { - operations = new List() - }; - - for (int i = 0; i < expectedCount; i++) - { - content.operations.Add( - new - { - op = "add", - data = new - { - type = "articles", - attributes = new - { - name = articles[i].Name - }, - relationships = new - { - author = new - { - data = new - { - type = "authors", - id = author.Id - } - } - } - } - } - ); - } - - // act - var (response, data) = await PatchAsync("api/bulk", content); - - // assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(expectedCount, data.Operations.Count); - - // author validation: sanity checks - var lastAuthor = context.Authors.Include(a => a.Articles).Single(a => a.Id == author.Id); - Assert.NotNull(lastAuthor); - Assert.Equal(author.Name, lastAuthor.Name); - - // articles validation - Assert.True(lastAuthor.Articles.Count == expectedCount); - for (int i = 0; i < expectedCount; i++) - { - var article = articles[i]; - Assert.NotNull(lastAuthor.Articles.FirstOrDefault(a => a.Name == article.Name)); - } - } - - [Fact] - public async Task Can_Create_Author_With_Article_Using_LocalId() - { - // arrange - var context = GetService(); - var author = AuthorFactory.Get(); - var article = ArticleFactory.Get(); - const string authorLocalId = "author-1"; - - var content = new - { - operations = new object[] { - new { - op = "add", - data = new { - lid = authorLocalId, - type = "authors", - attributes = new { - name = author.Name - }, - } - }, - new { - op = "add", - data = new { - type = "articles", - attributes = new { - name = article.Name - }, - relationships = new { - author = new { - data = new { - type = "authors", - lid = authorLocalId - } - } - } - } - } - } - }; - - // act - var (response, data) = await PatchAsync("api/bulk", content); - - // assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(2, data.Operations.Count); - - var authorOperationResult = data.Operations[0]; - var id = authorOperationResult.DataObject.Id; - var lastAuthor = await context.Authors - .Include(a => a.Articles) - .SingleAsync(a => a.StringId == id); - var articleOperationResult = data.Operations[1]; - - // author validation - Assert.Equal(authorLocalId, authorOperationResult.DataObject.LocalId); - Assert.Equal(author.Name, lastAuthor.Name); - - // article validation - Assert.Single(lastAuthor.Articles); - Assert.Equal(article.Name, lastAuthor.Articles[0].Name); - Assert.Equal(articleOperationResult.DataObject.Id, lastAuthor.Articles[0].StringId); - } - } -} diff --git a/test/OperationsExampleTests/Factories/ArticleFactory.cs b/test/OperationsExampleTests/Factories/ArticleFactory.cs deleted file mode 100644 index a03bc3fbea..0000000000 --- a/test/OperationsExampleTests/Factories/ArticleFactory.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using Bogus; -using JsonApiDotNetCoreExample.Models; - -namespace OperationsExampleTests.Factories -{ - public static class ArticleFactory - { - public static Article Get() - { - var faker = new Faker
(); - faker.RuleFor(m => m.Name, f => f.Lorem.Sentence()); - return faker.Generate(); - } - - public static List
Get(int count) - { - var articles = new List
(); - for (int i = 0; i < count; i++) - articles.Add(Get()); - - return articles; - } - } -} diff --git a/test/OperationsExampleTests/Factories/AuthorFactory.cs b/test/OperationsExampleTests/Factories/AuthorFactory.cs deleted file mode 100644 index e80b100a59..0000000000 --- a/test/OperationsExampleTests/Factories/AuthorFactory.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using Bogus; -using JsonApiDotNetCoreExample.Models; - -namespace OperationsExampleTests.Factories -{ - public static class AuthorFactory - { - public static Author Get() - { - var faker = new Faker(); - faker.RuleFor(m => m.Name, f => f.Person.UserName); - return faker.Generate(); - } - - public static List Get(int count) - { - var authors = new List(); - for (int i = 0; i < count; i++) - authors.Add(Get()); - - return authors; - } - } -} diff --git a/test/OperationsExampleTests/Fixture.cs b/test/OperationsExampleTests/Fixture.cs deleted file mode 100644 index 11621d36e4..0000000000 --- a/test/OperationsExampleTests/Fixture.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; -using JsonApiDotNetCoreExample.Data; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Newtonsoft.Json; -using Xunit; - -[assembly: CollectionBehavior(DisableTestParallelization = true)] -namespace OperationsExampleTests -{ - public class Fixture : IDisposable - { - public Fixture() - { - var builder = new WebHostBuilder().UseStartup(); - Server = new TestServer(builder); - Client = Server.CreateClient(); - } - - public TestServer Server { get; private set; } - public HttpClient Client { get; } - - public void Dispose() - { - try - { - var context = GetService(); - context.Articles.RemoveRange(context.Articles); - context.Authors.RemoveRange(context.Authors); - context.SaveChanges(); - } // it is possible the test may try to do something that is an invalid db operation - // validation should be left up to the test, so we should not bomb the run in the - // disposal of that context - catch (Exception) { } - } - - public T GetService() => (T)Server.Host.Services.GetService(typeof(T)); - - public async Task PatchAsync(string route, object data) - { - var httpMethod = new HttpMethod("PATCH"); - var request = new HttpRequestMessage(httpMethod, route); - request.Content = new StringContent(JsonConvert.SerializeObject(data)); - request.Content.Headers.ContentLength = 1; - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); - return await Client.SendAsync(request); - } - - public async Task<(HttpResponseMessage response, T data)> PatchAsync(string route, object data) - { - var response = await PatchAsync(route, data); - var json = await response.Content.ReadAsStringAsync(); - var obj = JsonConvert.DeserializeObject(json); - return (response, obj); - } - } -} diff --git a/test/OperationsExampleTests/Get/GetByIdTests.cs b/test/OperationsExampleTests/Get/GetByIdTests.cs deleted file mode 100644 index 1056082895..0000000000 --- a/test/OperationsExampleTests/Get/GetByIdTests.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Bogus; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models.Operations; -using JsonApiDotNetCoreExample.Data; -using OperationsExampleTests.Factories; -using Xunit; - -namespace OperationsExampleTests -{ - public class GetTests : Fixture, IDisposable - { - private readonly Faker _faker = new Faker(); - - [Fact] - public async Task Can_Get_Author_By_Id() - { - // arrange - var context = GetService(); - var author = AuthorFactory.Get(); - context.Authors.Add(author); - context.SaveChanges(); - - var content = new - { - operations = new[] { - new Dictionary { - { "op", "get"}, - { "ref", new { type = "authors", id = author.StringId } } - } - } - }; - - // act - var (response, data) = await PatchAsync("api/bulk", content); - - // assert - Assert.NotNull(response); - Assert.NotNull(data); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Single(data.Operations); - Assert.Equal(author.Id.ToString(), data.Operations.Single().DataObject.Id); - } - - [Fact] - public async Task Get_Author_By_Id_Returns_404_If_NotFound() - { - // arrange - var authorId = _faker.Random.Int(max: 0).ToString(); - - var content = new - { - operations = new[] { - new Dictionary { - { "op", "get"}, - { "ref", new { type = "authors", id = authorId } } - } - } - }; - - // act - var (response, data) = await PatchAsync("api/bulk", content); - - // assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - Assert.NotNull(data); - Assert.Single(data.Errors); - Assert.True(data.Errors[0].Detail.Contains("authors"), "The error detail should contain the name of the entity that could not be found."); - Assert.True(data.Errors[0].Detail.Contains(authorId), "The error detail should contain the entity id that could not be found"); - Assert.True(data.Errors[0].Title.Contains("operation[0]"), "The error title should contain the operation identifier that failed"); - } - } -} diff --git a/test/OperationsExampleTests/Get/GetRelationshipTests.cs b/test/OperationsExampleTests/Get/GetRelationshipTests.cs deleted file mode 100644 index 0aeef6f3ec..0000000000 --- a/test/OperationsExampleTests/Get/GetRelationshipTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Bogus; -using JsonApiDotNetCore.Models.Operations; -using JsonApiDotNetCoreExample.Data; -using OperationsExampleTests.Factories; -using Xunit; - -namespace OperationsExampleTests -{ - public class GetRelationshipTests : Fixture, IDisposable - { - private readonly Faker _faker = new Faker(); - - [Fact] - public async Task Can_Get_HasOne_Relationship() - { - // arrange - var context = GetService(); - var author = AuthorFactory.Get(); - var article = ArticleFactory.Get(); - article.Author = author; - context.Articles.Add(article); - context.SaveChanges(); - - var content = new - { - operations = new[] { - new Dictionary { - { "op", "get"}, - { "ref", new { type = "articles", id = article.StringId, relationship = "author" } } - } - } - }; - - // act - var (response, data) = await PatchAsync("api/bulk", content); - - // assert - Assert.NotNull(response); - Assert.NotNull(data); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Single(data.Operations); - var resourceObject = data.Operations.Single().DataObject; - Assert.Equal(author.Id.ToString(), resourceObject.Id); - Assert.Equal("authors", resourceObject.Type); - } - - [Fact] - public async Task Can_Get_HasMany_Relationship() - { - // arrange - var context = GetService(); - var author = AuthorFactory.Get(); - var article = ArticleFactory.Get(); - article.Author = author; - context.Articles.Add(article); - context.SaveChanges(); - - var content = new - { - operations = new[] { - new Dictionary { - { "op", "get"}, - { "ref", new { type = "authors", id = author.StringId, relationship = "articles" } } - } - } - }; - - // act - var (response, data) = await PatchAsync("api/bulk", content); - - // assert - Assert.NotNull(response); - Assert.NotNull(data); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Single(data.Operations); - - var resourceObject = data.Operations.Single().DataList.Single(); - Assert.Equal(article.Id.ToString(), resourceObject.Id); - Assert.Equal("articles", resourceObject.Type); - } - } -} diff --git a/test/OperationsExampleTests/Get/GetTests.cs b/test/OperationsExampleTests/Get/GetTests.cs deleted file mode 100644 index f0d3fdffd8..0000000000 --- a/test/OperationsExampleTests/Get/GetTests.cs +++ /dev/null @@ -1,73 +0,0 @@ - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Bogus; -using JsonApiDotNetCore.Models.Operations; -using JsonApiDotNetCoreExample.Data; -using OperationsExampleTests.Factories; -using Xunit; - -namespace OperationsExampleTests -{ - public class GetByIdTests : Fixture, IDisposable - { - private readonly Faker _faker = new Faker(); - - [Fact] - public async Task Can_Get_Authors() - { - // arrange - var expectedCount = _faker.Random.Int(1, 10); - var context = GetService(); - context.Articles.RemoveRange(context.Articles); - context.Authors.RemoveRange(context.Authors); - var authors = AuthorFactory.Get(expectedCount); - context.AddRange(authors); - context.SaveChanges(); - - var content = new - { - operations = new[] { - new Dictionary { - { "op", "get"}, - { "ref", new { type = "authors" } } - } - } - }; - - // act - var result = await PatchAsync("api/bulk", content); - - // assert - Assert.NotNull(result.response); - Assert.NotNull(result.data); - Assert.Equal(HttpStatusCode.OK, result.response.StatusCode); - Assert.Single(result.data.Operations); - Assert.Equal(expectedCount, result.data.Operations.Single().DataList.Count); - } - - [Fact] - public async Task Get_Non_Existent_Type_Returns_400() - { - // arrange - var content = new - { - operations = new[] { - new Dictionary { - { "op", "get"}, - { "ref", new { type = "non-existent-type" } } - } - } - }; - - // act - var result = await PatchAsync("api/bulk", content); - - // assert - Assert.Equal(HttpStatusCode.BadRequest, result.response.StatusCode); - } - } -} diff --git a/test/OperationsExampleTests/OperationsExampleTests.csproj b/test/OperationsExampleTests/OperationsExampleTests.csproj deleted file mode 100644 index f84b550354..0000000000 --- a/test/OperationsExampleTests/OperationsExampleTests.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - $(NetCoreAppVersion) - false - OperationsExampleTests - - - - - - - - - - - - - - - - PreserveNewest - - - diff --git a/test/OperationsExampleTests/Remove/RemoveTests.cs b/test/OperationsExampleTests/Remove/RemoveTests.cs deleted file mode 100644 index b5e0cffaf3..0000000000 --- a/test/OperationsExampleTests/Remove/RemoveTests.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Bogus; -using JsonApiDotNetCore.Models.Operations; -using JsonApiDotNetCoreExample.Data; -using OperationsExampleTests.Factories; -using Xunit; - -namespace OperationsExampleTests -{ - public class RemoveTests : Fixture - { - private readonly Faker _faker = new Faker(); - - [Fact] - public async Task Can_Remove_Author() - { - // arrange - var context = GetService(); - var author = AuthorFactory.Get(); - context.Authors.Add(author); - context.SaveChanges(); - - var content = new - { - operations = new[] { - new Dictionary { - { "op", "remove"}, - { "ref", new { type = "authors", id = author.StringId } } - } - } - }; - - // act - var (response, data) = await PatchAsync("api/bulk", content); - - // assert - Assert.NotNull(response); - Assert.NotNull(data); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Empty(data.Operations); - Assert.Null(context.Authors.SingleOrDefault(a => a.Id == author.Id)); - } - - [Fact] - public async Task Can_Remove_Authors() - { - // arrange - var count = _faker.Random.Int(1, 10); - var context = GetService(); - - var authors = AuthorFactory.Get(count); - - context.Authors.AddRange(authors); - context.SaveChanges(); - - var content = new - { - operations = new List() - }; - - for (int i = 0; i < count; i++) - content.operations.Add( - new Dictionary { - { "op", "remove"}, - { "ref", new { type = "authors", id = authors[i].StringId } } - } - ); - - // act - var (response, data) = await PatchAsync("api/bulk", content); - - // assert - Assert.NotNull(response); - Assert.NotNull(data); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Empty(data.Operations); - - for (int i = 0; i < count; i++) - Assert.Null(context.Authors.SingleOrDefault(a => a.Id == authors[i].Id)); - } - } -} diff --git a/test/OperationsExampleTests/TestStartup.cs b/test/OperationsExampleTests/TestStartup.cs deleted file mode 100644 index 449c193177..0000000000 --- a/test/OperationsExampleTests/TestStartup.cs +++ /dev/null @@ -1,25 +0,0 @@ -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCoreExample.Data; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using OperationsExample; -using System; -using UnitTests; - -namespace OperationsExampleTests -{ - public class TestStartup : Startup - { - public TestStartup(IHostingEnvironment env) : base(env) - { } - - public override IServiceProvider ConfigureServices(IServiceCollection services) - { - base.ConfigureServices(services); - services.AddScoped(); - services.AddSingleton>(); - return services.BuildServiceProvider(); - } - } -} diff --git a/test/OperationsExampleTests/Transactions/TransactionFailureTests.cs b/test/OperationsExampleTests/Transactions/TransactionFailureTests.cs deleted file mode 100644 index 191711651d..0000000000 --- a/test/OperationsExampleTests/Transactions/TransactionFailureTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Bogus; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCoreExample.Data; -using Microsoft.EntityFrameworkCore; -using OperationsExampleTests.Factories; -using Xunit; - -namespace OperationsExampleTests -{ - public class TransactionFailureTests : Fixture - { - private readonly Faker _faker = new Faker(); - - [Fact] - public async Task Cannot_Create_Author_If_Article_Creation_Fails() - { - // arrange - var context = GetService(); - var author = AuthorFactory.Get(); - var article = ArticleFactory.Get(); - - // do this so that the name is random enough for db validations - author.Name = Guid.NewGuid().ToString("N"); - article.Name = Guid.NewGuid().ToString("N"); - - var content = new - { - operations = new object[] { - new { - op = "add", - data = new { - type = "authors", - attributes = new { - name = author.Name - }, - } - }, - new { - op = "add", - data = new { - type = "articles", - attributes = new { - name = article.Name - }, - // by not including the author, the article creation will fail - // relationships = new { - // author = new { - // data = new { - // type = "authors", - // lid = authorLocalId - // } - // } - // } - } - } - } - }; - - // act - var (response, data) = await PatchAsync("api/bulk", content); - - // assert - Assert.NotNull(response); - // for now, it is up to application implementations to perform validation and - // provide the proper HTTP response code - Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); - Assert.Single(data.Errors); - Assert.Contains("operation[1] (add)", data.Errors[0].Title); - - var dbAuthors = await context.Authors.Where(a => a.Name == author.Name).ToListAsync(); - var dbArticles = await context.Articles.Where(a => a.Name == article.Name).ToListAsync(); - Assert.Empty(dbAuthors); - Assert.Empty(dbArticles); - } - } -} diff --git a/test/OperationsExampleTests/Update/UpdateTests.cs b/test/OperationsExampleTests/Update/UpdateTests.cs deleted file mode 100644 index c5d220b2f5..0000000000 --- a/test/OperationsExampleTests/Update/UpdateTests.cs +++ /dev/null @@ -1,112 +0,0 @@ -using Bogus; -using JsonApiDotNetCore.Models.Operations; -using JsonApiDotNetCoreExample.Data; -using OperationsExampleTests.Factories; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Xunit; - -namespace OperationsExampleTests.Update -{ - public class UpdateTests : Fixture - { - private readonly Faker _faker = new Faker(); - - [Fact] - public async Task Can_Update_Author() - { - // arrange - var context = GetService(); - var author = AuthorFactory.Get(); - var updates = AuthorFactory.Get(); - context.Authors.Add(author); - context.SaveChanges(); - - var content = new - { - operations = new[] { - new Dictionary { - { "op", "update" }, - { "ref", new { - type = "authors", - id = author.Id, - } }, - { "data", new { - type = "authors", - id = author.Id, - attributes = new - { - name = updates.Name - } - } }, - } - } - }; - - // act - var (response, data) = await PatchAsync("api/bulk", content); - - // assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(data); - Assert.Single(data.Operations); - - var attrs = data.Operations.Single().DataObject.Attributes; - Assert.Equal(updates.Name, attrs["name"]); - } - - [Fact] - public async Task Can_Update_Authors() - { - // arrange - var count = _faker.Random.Int(1, 10); - var context = GetService(); - - var authors = AuthorFactory.Get(count); - var updates = AuthorFactory.Get(count); - - context.Authors.AddRange(authors); - context.SaveChanges(); - - var content = new - { - operations = new List() - }; - - for (int i = 0; i < count; i++) - content.operations.Add(new Dictionary { - { "op", "update" }, - { "ref", new { - type = "authors", - id = authors[i].Id, - } }, - { "data", new { - type = "authors", - id = authors[i].Id, - attributes = new - { - name = updates[i].Name - } - } }, - }); - - // act - var (response, data) = await PatchAsync("api/bulk", content); - - // assert - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(data); - Assert.Equal(count, data.Operations.Count); - - for (int i = 0; i < count; i++) - { - var attrs = data.Operations[i].DataObject.Attributes; - Assert.Equal(updates[i].Name, attrs["name"]); - } - } - } -} diff --git a/test/OperationsExampleTests/appsettings.json b/test/OperationsExampleTests/appsettings.json deleted file mode 100644 index 84f8cf4220..0000000000 --- a/test/OperationsExampleTests/appsettings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "Data": { - "DefaultConnection": - "Host=localhost;Port=5432;Database=JsonApiDotNetCoreExample;User ID=postgres;Password=postgres" - }, - "Logging": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Warning", - "System": "Warning", - "Microsoft": "Warning", - "JsonApiDotNetCore.Middleware.JsonApiExceptionFilter": "Critical" - } - } -} diff --git a/test/ResourceEntitySeparationExampleTests/Acceptance/AddTests.cs b/test/ResourceEntitySeparationExampleTests/Acceptance/AddTests.cs deleted file mode 100644 index 53105ee4b6..0000000000 --- a/test/ResourceEntitySeparationExampleTests/Acceptance/AddTests.cs +++ /dev/null @@ -1,212 +0,0 @@ -using JsonApiDotNetCoreExample.Models.Resources; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization.Infrastructure; -using Xunit; - -namespace ResourceEntitySeparationExampleTests.Acceptance -{ - [Collection("TestCollection")] - public class AddTests - { - private readonly TestFixture _fixture; - - public AddTests(TestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task Can_Create_Course() - { - // arrange - var route = $"/api/v1/courses/"; - var course = _fixture.CourseFaker.Generate(); - var content = new - { - data = new - { - type = "courses", - attributes = new Dictionary() - { - { "number", course.Number }, - { "title", course.Title }, - { "description", course.Description } - } - } - }; - - // act - var (response, data) = await _fixture.PostAsync(route, content); - - // assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - Assert.NotNull(data); - Assert.Equal(course.Number, data.Number); - Assert.Equal(course.Title, data.Title); - Assert.Equal(course.Description, data.Description); - } - - [Fact] - public async Task Can_Create_Course_With_Department_Id() - { - // arrange - var route = $"/api/v1/courses/"; - var course = _fixture.CourseFaker.Generate(); - - var department = _fixture.DepartmentFaker.Generate(); - _fixture.Context.Departments.Add(department); - _fixture.Context.SaveChanges(); - - var content = new - { - data = new - { - type = "courses", - attributes = new Dictionary - { - { "number", course.Number }, - { "title", course.Title }, - { "description", course.Description } - }, - relationships = new - { - department = new - { - data = new - { - type = "departments", - id = department.Id - } - } - } - } - }; - - // act - var (response, data) = await _fixture.PostAsync(route, content); - - // assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - Assert.NotNull(data); - Assert.Equal(course.Number, data.Number); - Assert.Equal(course.Title, data.Title); - Assert.Equal(course.Description, data.Description); - Assert.Equal(department.Id, data.Department.Id); - } - - [Fact] - public async Task Can_Create_Department() - { - // arrange - var route = $"/api/v1/departments/"; - var dept = _fixture.DepartmentFaker.Generate(); - var content = new - { - data = new - { - type = "departments", - attributes = new Dictionary() - { - { "name", dept.Name } - } - } - }; - - // act - var (response, data) = await _fixture.PostAsync(route, content); - - // assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - Assert.NotNull(data); - Assert.Equal(dept.Name, data.Name); - } - - [Fact] - public async Task Can_Create_Department_With_Courses() - { - // arrange - var route = $"/api/v1/departments/"; - var dept = _fixture.DepartmentFaker.Generate(); - - var one = _fixture.CourseFaker.Generate(); - var two = _fixture.CourseFaker.Generate(); - _fixture.Context.Courses.Add(one); - _fixture.Context.Courses.Add(two); - _fixture.Context.SaveChanges(); - - var content = new - { - data = new - { - type = "departments", - attributes = new Dictionary - { - { "name", dept.Name } - }, - relationships = new - { - courses = new - { - data = new[] - { - new - { - type = "courses", - id = one.Id - }, - new - { - type = "courses", - id = two.Id - } - } - } - } - } - }; - - // act - var (response, data) = await _fixture.PostAsync(route, content); - - // assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - Assert.NotNull(data); - Assert.Equal(dept.Name, data.Name); - Assert.NotEmpty(data.Courses); - Assert.NotNull(data.Courses.SingleOrDefault(c => c.Id == one.Id)); - Assert.NotNull(data.Courses.SingleOrDefault(c => c.Id == two.Id)); - } - - [Fact] - public async Task Can_Create_Student() - { - // arrange - var route = $"/api/v1/students/"; - var student = _fixture.StudentFaker.Generate(); - var content = new - { - data = new - { - type = "students", - attributes = new Dictionary() - { - { "firstname", student.FirstName }, - { "lastname", student.LastName } - } - } - }; - - // act - var (response, data) = await _fixture.PostAsync(route, content); - - // assert - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - Assert.NotNull(data); - Assert.Equal(student.FirstName, data.FirstName); - Assert.Equal(student.LastName, data.LastName); - } - } -} diff --git a/test/ResourceEntitySeparationExampleTests/Acceptance/DeleteTests.cs b/test/ResourceEntitySeparationExampleTests/Acceptance/DeleteTests.cs deleted file mode 100644 index a5602e9936..0000000000 --- a/test/ResourceEntitySeparationExampleTests/Acceptance/DeleteTests.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Net; -using System.Threading.Tasks; -using Xunit; - -namespace ResourceEntitySeparationExampleTests.Acceptance -{ - [Collection("TestCollection")] - public class DeleteTests - { - private readonly TestFixture _fixture; - - public DeleteTests(TestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task Can_Delete_Course() - { - // arrange - var course = _fixture.CourseFaker.Generate(); - _fixture.Context.Courses.Add(course); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/courses/{course.Id}"; - - // act - var response = await _fixture.DeleteAsync(route); - - // assert - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - } - - [Fact] - public async Task Can_Delete_Department() - { - // arrange - var dept = _fixture.DepartmentFaker.Generate(); - _fixture.Context.Departments.Add(dept); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/departments/{dept.Id}"; - - // act - var response = await _fixture.DeleteAsync(route); - - // assert - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - } - - [Fact] - public async Task Can_Delete_Student() - { - // arrange - var student = _fixture.StudentFaker.Generate(); - _fixture.Context.Students.Add(student); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/students/{student.Id}"; - - // act - var response = await _fixture.DeleteAsync(route); - - // assert - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - } - } -} diff --git a/test/ResourceEntitySeparationExampleTests/Acceptance/GetTests.cs b/test/ResourceEntitySeparationExampleTests/Acceptance/GetTests.cs deleted file mode 100644 index 371a07ab9b..0000000000 --- a/test/ResourceEntitySeparationExampleTests/Acceptance/GetTests.cs +++ /dev/null @@ -1,265 +0,0 @@ -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCoreExample.Models.Entities; -using JsonApiDotNetCoreExample.Models.Resources; -using JsonApiDotNetCoreExampleTests.Helpers.Extensions; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Xunit; - -namespace ResourceEntitySeparationExampleTests.Acceptance -{ - [Collection("TestCollection")] - public class GetTests - { - private readonly TestFixture _fixture; - - public GetTests(TestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task Can_Get_Courses() - { - // arrange - var course = _fixture.CourseFaker.Generate(); - _fixture.Context.Courses.Add(course); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/courses"; - - // act - var response = await _fixture.SendAsync("GET", route, null); - var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() - .DeserializeList(responseBody); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(deserializedBody); - Assert.NotEmpty(deserializedBody); - } - - [Fact] - public async Task Can_Get_Course_By_Id() - { - // arrange - var course = _fixture.CourseFaker.Generate(); - _fixture.Context.Courses.Add(course); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/courses/{course.Id}"; - - // act - var (response, data) = await _fixture.GetAsync(route); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(data); - Assert.Equal(course.Number, data.Number); - Assert.Equal(course.Title, data.Title); - Assert.Equal(course.Description, data.Description); - } - - [Fact] - public async Task Can_Get_Course_With_Relationships() - { - // arrange - var dept = _fixture.DepartmentFaker.Generate(); - _fixture.Context.Departments.Add(dept); - - var course = _fixture.CourseFaker.Generate(); - course.Department = dept; - _fixture.Context.Courses.Add(course); - - var student = _fixture.StudentFaker.Generate(); - _fixture.Context.Students.Add(student); - - var reg = new CourseStudentEntity(course, student); - _fixture.Context.Registrations.Add(reg); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/courses/{course.Id}?include=students,department"; - - // act - var (response, data) = await _fixture.GetAsync(route); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(data); - - Assert.Equal(course.Number, data.Number); - Assert.Equal(course.Title, data.Title); - Assert.Equal(course.Description, data.Description); - - Assert.NotEmpty(data.Students); - Assert.NotNull(data.Students[0]); - Assert.Equal(student.Id, data.Students[0].Id); - Assert.Equal(student.LastName, data.Students[0].LastName); - - Assert.NotNull(data.Department); - Assert.Equal(dept.Id, data.Department.Id); - Assert.Equal(dept.Name, data.Department.Name); - } - - [Fact] - public async Task Can_Get_Departments() - { - // arrange - var dept = _fixture.DepartmentFaker.Generate(); - _fixture.Context.Departments.Add(dept); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/departments"; - - // act - var response = await _fixture.SendAsync("GET", route, null); - var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() - .DeserializeList(responseBody); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(deserializedBody); - Assert.NotEmpty(deserializedBody); - } - - [Fact] - public async Task Can_Get_Department_By_Id() - { - // arrange - var dept = _fixture.DepartmentFaker.Generate(); - _fixture.Context.Departments.Add(dept); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/departments/{dept.Id}"; - - // act - var (response, data) = await _fixture.GetAsync(route); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(data); - Assert.Equal(dept.Id, data.Id); - Assert.Equal(dept.Name, data.Name); - } - - [Fact] - public async Task Can_Get_Department_With_Relationships() - { - // arrange - var dept = _fixture.DepartmentFaker.Generate(); - _fixture.Context.Departments.Add(dept); - - var course = _fixture.CourseFaker.Generate(); - course.Department = dept; - _fixture.Context.Courses.Add(course); - - var othercourse = _fixture.CourseFaker.Generate(); - othercourse.Department = dept; - _fixture.Context.Courses.Add(othercourse); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/departments/{dept.Id}?include=courses"; - - // act - var (response, data) = await _fixture.GetAsync(route); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(data); - - Assert.Equal(dept.Id, data.Id); - Assert.Equal(dept.Name, data.Name); - - Assert.NotEmpty(data.Courses); - Assert.Equal(2, data.Courses.Count); - } - - [Fact] - public async Task Can_Get_Students() - { - // arrange - var student = _fixture.StudentFaker.Generate(); - _fixture.Context.Students.Add(student); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/students"; - - // act - var response = await _fixture.SendAsync("GET", route, null); - var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() - .DeserializeList(responseBody); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(deserializedBody); - Assert.NotEmpty(deserializedBody); - } - - [Fact] - public async Task Can_Get_Student_By_Id() - { - // arrange - var student = _fixture.StudentFaker.Generate(); - _fixture.Context.Students.Add(student); - _fixture.Context.SaveChanges(); - - var httpMethod = new HttpMethod("GET"); - var route = $"/api/v1/students/{student.Id}"; - - var request = new HttpRequestMessage(httpMethod, route); - - // act - var response = await _fixture.Server.CreateClient().SendAsync(request); - var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = (StudentResource)_fixture.Server.GetService() - .Deserialize(responseBody); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(deserializedBody); - Assert.Equal(student.FirstName, deserializedBody.FirstName); - Assert.Equal(student.LastName, deserializedBody.LastName); - } - - [Fact] - public async Task Can_Get_Student_With_Relationships() - { - // arrange - var course = _fixture.CourseFaker.Generate(); - _fixture.Context.Courses.Add(course); - _fixture.Context.SaveChanges(); - - var student = _fixture.StudentFaker.Generate(); - _fixture.Context.Students.Add(student); - _fixture.Context.SaveChanges(); - - var reg = new CourseStudentEntity(course, student); - _fixture.Context.Registrations.Add(reg); - _fixture.Context.SaveChanges(); - - var httpMethod = new HttpMethod("GET"); - var route = $"/api/v1/students/{student.Id}?include=courses"; - - var request = new HttpRequestMessage(httpMethod, route); - - // act - var response = await _fixture.Server.CreateClient().SendAsync(request); - var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = (StudentResource)_fixture.Server.GetService() - .Deserialize(responseBody); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(deserializedBody); - - Assert.Equal(student.FirstName, deserializedBody.FirstName); - Assert.Equal(student.LastName, deserializedBody.LastName); - - Assert.NotEmpty(deserializedBody.Courses); - } - } -} diff --git a/test/ResourceEntitySeparationExampleTests/Acceptance/RelationshipGetTests.cs b/test/ResourceEntitySeparationExampleTests/Acceptance/RelationshipGetTests.cs deleted file mode 100644 index 0beb6c3b6b..0000000000 --- a/test/ResourceEntitySeparationExampleTests/Acceptance/RelationshipGetTests.cs +++ /dev/null @@ -1,224 +0,0 @@ -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCoreExample.Models.Entities; -using JsonApiDotNetCoreExample.Models.Resources; -using JsonApiDotNetCoreExampleTests.Helpers.Extensions; -using System.Net; -using System.Threading.Tasks; -using Xunit; - -namespace ResourceEntitySeparationExampleTests.Acceptance -{ - [Collection("TestCollection")] - public class RelationshipGetTests - { - private readonly TestFixture _fixture; - - public RelationshipGetTests(TestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task Can_Get_Courses_For_Department() - { - // arrange - var dept = _fixture.DepartmentFaker.Generate(); - var course = _fixture.CourseFaker.Generate(); - course.Department = dept; - _fixture.Context.Courses.Add(course); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/departments/{dept.Id}/courses"; - - // act - var response = await _fixture.SendAsync("GET", route, null); - var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() - .DeserializeList(responseBody); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(deserializedBody); - Assert.NotEmpty(deserializedBody); - } - - [Fact] - public async Task Can_Get_Course_Relationships_For_Department() - { - // arrange - var dept = _fixture.DepartmentFaker.Generate(); - var course = _fixture.CourseFaker.Generate(); - course.Department = dept; - _fixture.Context.Courses.Add(course); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/departments/{dept.Id}/relationships/courses"; - - // act - var response = await _fixture.SendAsync("GET", route, null); - var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() - .DeserializeList(responseBody); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(deserializedBody); - Assert.NotEmpty(deserializedBody); - } - - [Fact] - public async Task Can_Get_Courses_For_Student() - { - // arrange - var course = _fixture.CourseFaker.Generate(); - _fixture.Context.Courses.Add(course); - - var student = _fixture.StudentFaker.Generate(); - _fixture.Context.Students.Add(student); - - var reg = new CourseStudentEntity(course, student); - _fixture.Context.Registrations.Add(reg); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/students/{student.Id}/courses"; - - // act - var response = await _fixture.SendAsync("GET", route, null); - var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() - .DeserializeList(responseBody); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(deserializedBody); - Assert.NotEmpty(deserializedBody); - } - - [Fact] - public async Task Can_Get_Course_Relationships_For_Student() - { - // arrange - var course = _fixture.CourseFaker.Generate(); - _fixture.Context.Courses.Add(course); - - var student = _fixture.StudentFaker.Generate(); - _fixture.Context.Students.Add(student); - - var reg = new CourseStudentEntity(course, student); - _fixture.Context.Registrations.Add(reg); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/students/{student.Id}/relationships/courses"; - - // act - var response = await _fixture.SendAsync("GET", route, null); - var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() - .DeserializeList(responseBody); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(deserializedBody); - Assert.NotEmpty(deserializedBody); - } - - [Fact] - public async Task Can_Get_Department_For_Course() - { - // arrange - var dept = _fixture.DepartmentFaker.Generate(); - var course = _fixture.CourseFaker.Generate(); - course.Department = dept; - _fixture.Context.Courses.Add(course); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/courses/{course.Id}/department"; - - // act - var (response, data) = await _fixture.GetAsync(route); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(data); - Assert.Equal(dept.Name, data.Name); - } - - [Fact] - public async Task Can_Get_Department_Relationships_For_Course() - { - // arrange - var dept = _fixture.DepartmentFaker.Generate(); - var course = _fixture.CourseFaker.Generate(); - course.Department = dept; - _fixture.Context.Courses.Add(course); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/courses/{course.Id}/relationships/department"; - - // act - var (response, data) = await _fixture.GetAsync(route); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(data); - } - - [Fact] - public async Task Can_Get_Students_For_Course() - { - // arrange - var course = _fixture.CourseFaker.Generate(); - _fixture.Context.Courses.Add(course); - _fixture.Context.SaveChanges(); - - var student = _fixture.StudentFaker.Generate(); - _fixture.Context.Students.Add(student); - _fixture.Context.SaveChanges(); - - var reg = new CourseStudentEntity(course, student); - _fixture.Context.Registrations.Add(reg); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/courses/{course.Id}/students"; - - // act - var response = await _fixture.SendAsync("GET", route, null); - var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() - .DeserializeList(responseBody); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(deserializedBody); - Assert.NotEmpty(deserializedBody); - } - - [Fact] - public async Task Can_Get_Student_Relationships_For_Course() - { - // arrange - var course = _fixture.CourseFaker.Generate(); - _fixture.Context.Courses.Add(course); - - var student = _fixture.StudentFaker.Generate(); - _fixture.Context.Students.Add(student); - - var reg = new CourseStudentEntity(course, student); - _fixture.Context.Registrations.Add(reg); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/courses/{course.Id}/relationships/students"; - - // act - var response = await _fixture.SendAsync("GET", route, null); - var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() - .DeserializeList(responseBody); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(deserializedBody); - Assert.NotEmpty(deserializedBody); - } - } -} diff --git a/test/ResourceEntitySeparationExampleTests/Acceptance/RelationshipModifyTests.cs b/test/ResourceEntitySeparationExampleTests/Acceptance/RelationshipModifyTests.cs deleted file mode 100644 index 2fd6ffc81d..0000000000 --- a/test/ResourceEntitySeparationExampleTests/Acceptance/RelationshipModifyTests.cs +++ /dev/null @@ -1,95 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; -using Xunit; - -namespace ResourceEntitySeparationExampleTests.Acceptance -{ - [Collection("TestCollection")] - public class RelationshipModifyTests - { - private readonly TestFixture _fixture; - - public RelationshipModifyTests(TestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task Can_Patch_HasOne_Relationship() - { - // arrange - var dept = _fixture.DepartmentFaker.Generate(); - _fixture.Context.Departments.Add(dept); - - var course = _fixture.CourseFaker.Generate(); - _fixture.Context.Courses.Add(course); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/courses/{course.Id}/relationships/department"; - var content = new - { - data = new - { - type = "departments", - id = $"{dept.Id}" - } - }; - - // act - var response = await _fixture.SendAsync("PATCH", route, content); - var responseBody = await response.Content.ReadAsStringAsync(); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - _fixture.Context.Entry(course).Reload(); - Assert.Equal(dept.Id, course.DepartmentId); - } - - [Fact] - public async Task Can_Patch_HasMany_Relationship() - { - // arrange - var dept = _fixture.DepartmentFaker.Generate(); - _fixture.Context.Departments.Add(dept); - - var course1 = _fixture.CourseFaker.Generate(); - _fixture.Context.Courses.Add(course1); - var course2 = _fixture.CourseFaker.Generate(); - _fixture.Context.Courses.Add(course2); - _fixture.Context.SaveChanges(); - - var route = $"/api/v1/departments/{dept.Id}/relationships/courses"; - var content = new - { - data = new List - { - new { - type = "courses", - id = $"{course1.Id}" - }, - new { - type = "courses", - id = $"{course2.Id}" - } - } - }; - - // act - var response = await _fixture.SendAsync("PATCH", route, content); - var responseBody = await response.Content.ReadAsStringAsync(); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - _fixture.Context.Entry(course1).Reload(); - _fixture.Context.Entry(course2).Reload(); - Assert.Equal(dept.Id, course1.DepartmentId); - Assert.Equal(dept.Id, course2.DepartmentId); - } - } -} diff --git a/test/ResourceEntitySeparationExampleTests/Acceptance/UpdateTests.cs b/test/ResourceEntitySeparationExampleTests/Acceptance/UpdateTests.cs deleted file mode 100644 index eeb838c475..0000000000 --- a/test/ResourceEntitySeparationExampleTests/Acceptance/UpdateTests.cs +++ /dev/null @@ -1,121 +0,0 @@ -using JsonApiDotNetCoreExample.Models.Resources; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using Xunit; - -namespace ResourceEntitySeparationExampleTests.Acceptance -{ - [Collection("TestCollection")] - public class UpdateTests - { - private readonly TestFixture _fixture; - - public UpdateTests(TestFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task Can_Update_Course() - { - // arrange - var course = _fixture.CourseFaker.Generate(); - _fixture.Context.Courses.Add(course); - _fixture.Context.SaveChanges(); - - var updatedCourse = _fixture.CourseFaker.Generate(); - - var route = $"/api/v1/courses/{course.Id}"; - var content = new - { - data = new - { - id = course.Id, - type = "courses", - attributes = new Dictionary() - { - { "number", updatedCourse.Number } - } - } - }; - - // act - var (response, data) = await _fixture.PatchAsync(route, content); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(data); - Assert.Equal(course.Id, data.Id); - Assert.Equal(updatedCourse.Number, data.Number); - } - - [Fact] - public async Task Can_Update_Department() - { - // arrange - var dept = _fixture.DepartmentFaker.Generate(); - _fixture.Context.Departments.Add(dept); - _fixture.Context.SaveChanges(); - - var updatedDept = _fixture.DepartmentFaker.Generate(); - - var route = $"/api/v1/departments/{dept.Id}"; - var content = new - { - data = new - { - id = dept.Id, - type = "departments", - attributes = new Dictionary() - { - { "name", updatedDept.Name } - } - } - }; - - // act - var (response, data) = await _fixture.PatchAsync(route, content); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(data); - Assert.Equal(dept.Id, data.Id); - Assert.Equal(updatedDept.Name, data.Name); - } - - [Fact] - public async Task Can_Update_Student() - { - // arrange - var student = _fixture.StudentFaker.Generate(); - _fixture.Context.Students.Add(student); - _fixture.Context.SaveChanges(); - - var updatedStudent = _fixture.StudentFaker.Generate(); - - var route = $"/api/v1/students/{student.Id}"; - var content = new - { - data = new - { - id = student.Id, - type = "students", - attributes = new Dictionary() - { - { "lastname", updatedStudent.LastName } - } - } - }; - - // act - var (response, data) = await _fixture.PatchAsync(route, content); - - // assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(data); - Assert.Equal(student.Id, data.Id); - Assert.Equal(updatedStudent.LastName, data.LastName); - } - } -} diff --git a/test/ResourceEntitySeparationExampleTests/ResourceEntitySeparationExampleTests.csproj b/test/ResourceEntitySeparationExampleTests/ResourceEntitySeparationExampleTests.csproj deleted file mode 100644 index cfa496283e..0000000000 --- a/test/ResourceEntitySeparationExampleTests/ResourceEntitySeparationExampleTests.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - netcoreapp2.0 - - false - - - - - - - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers - all - - - - diff --git a/test/ResourceEntitySeparationExampleTests/TestCollection.cs b/test/ResourceEntitySeparationExampleTests/TestCollection.cs deleted file mode 100644 index 42a16eb67a..0000000000 --- a/test/ResourceEntitySeparationExampleTests/TestCollection.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Xunit; - -[assembly: CollectionBehavior(DisableTestParallelization = true)] -namespace ResourceEntitySeparationExampleTests -{ - [CollectionDefinition("TestCollection")] - public class TestCollection : ICollectionFixture - { } -} diff --git a/test/ResourceEntitySeparationExampleTests/TestFixture.cs b/test/ResourceEntitySeparationExampleTests/TestFixture.cs deleted file mode 100644 index d54fe43688..0000000000 --- a/test/ResourceEntitySeparationExampleTests/TestFixture.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Bogus; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCoreExample.Data; -using JsonApiDotNetCoreExample.Models.Entities; -using JsonApiDotNetCoreExampleTests.Helpers.Extensions; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Newtonsoft.Json; -using System; -using System.Collections; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; - -namespace ResourceEntitySeparationExampleTests -{ - public class TestFixture : IDisposable - { - public HttpClient Client { get; private set; } - public AppDbContext Context { get; private set; } - public TestServer Server { get; private set; } - - public Faker CourseFaker { get; private set; } - public Faker DepartmentFaker { get; private set; } - public Faker StudentFaker { get; private set; } - - public TestFixture() - { - var builder = new WebHostBuilder().UseStartup(); - Server = new TestServer(builder); - Context = Server.GetService(); - Context.Database.EnsureCreated(); - Client = Server.CreateClient(); - - CourseFaker = new Faker() - .RuleFor(c => c.Number, f => f.Random.Int(min: 0, max: 1000)) - .RuleFor(c => c.Title, f => f.Name.JobArea()) - .RuleFor(c => c.Description, f => f.Lorem.Paragraph()); - - DepartmentFaker = new Faker() - .RuleFor(d => d.Name, f => f.Commerce.Department()); - - StudentFaker = new Faker() - .RuleFor(s => s.FirstName, f => f.Name.FirstName()) - .RuleFor(s => s.LastName, f => f.Name.LastName()); - } - - public void Dispose() - { - Server.Dispose(); - } - - public async Task DeleteAsync(string route) - { - return await SendAsync("DELETE", route, null); - } - - public async Task<(HttpResponseMessage response, T data)> GetAsync(string route) - { - return await SendAsync("GET", route, null); - } - - public async Task<(HttpResponseMessage response, T data)> PatchAsync(string route, object data) - { - return await SendAsync("PATCH", route, data); - } - - public async Task<(HttpResponseMessage response, T data)> PostAsync(string route, object data) - { - return await SendAsync("POST", route, data); - } - - public async Task SendAsync(string method, string route, object data) - { - var httpMethod = new HttpMethod(method); - var request = new HttpRequestMessage(httpMethod, route) - { - Content = new StringContent(JsonConvert.SerializeObject(data)) - }; - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); - return await Client.SendAsync(request); - } - - public async Task<(HttpResponseMessage response, T data)> SendAsync(string method, string route, object data) - { - var response = await SendAsync(method, route, data); - var json = await response.Content.ReadAsStringAsync(); - var obj = (T)Server.GetService().Deserialize(json); - return (response, obj); - } - } -} diff --git a/test/ResourceEntitySeparationExampleTests/TestStartup.cs b/test/ResourceEntitySeparationExampleTests/TestStartup.cs deleted file mode 100644 index c7b74ad55d..0000000000 --- a/test/ResourceEntitySeparationExampleTests/TestStartup.cs +++ /dev/null @@ -1,22 +0,0 @@ -using JsonApiDotNetCore.Services; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using ResourceEntitySeparationExample; -using System; -using UnitTests; - -namespace ResourceEntitySeparationExampleTests -{ - public class TestStartup : Startup - { - public TestStartup(IHostingEnvironment env) : base(env) - { } - - public override IServiceProvider ConfigureServices(IServiceCollection services) - { - base.ConfigureServices(services); - services.AddScoped(); - return services.BuildServiceProvider(); - } - } -} diff --git a/test/ResourceEntitySeparationExampleTests/appsettings.json b/test/ResourceEntitySeparationExampleTests/appsettings.json deleted file mode 100644 index 603b456dc6..0000000000 --- a/test/ResourceEntitySeparationExampleTests/appsettings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "Data": { - "DefaultConnection": - "Host=localhost;Port=5432;Database=JsonApiDotNetCoreExample;User ID=postgres;Password=postgres" - }, - "Logging": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Debug", - "System": "Warning", - "Microsoft": "Warning", - "JsonApiDotNetCore.Middleware.JsonApiExceptionFilter": "Critical" - } - } -} diff --git a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs index a88ecceb67..fae27307d2 100644 --- a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs +++ b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs @@ -4,10 +4,9 @@ using System.Reflection; using Humanizer; using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Graph; -using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -17,119 +16,110 @@ namespace UnitTests { public class ResourceGraphBuilder_Tests { - class NonDbResource : Identifiable {} - class DbResource : Identifiable {} - class TestContext : DbContext { - public DbSet DbResources { get; set; } - } - - public ResourceGraphBuilder_Tests() + class NonDbResource : Identifiable { } + class DbResource : Identifiable { } + class TestContext : DbContext { - JsonApiOptions.ResourceNameFormatter = new DefaultResourceNameFormatter(); + public DbSet DbResources { get; set; } } [Fact] public void Can_Build_ResourceGraph_Using_Builder() { - // arrange + // Arrange var services = new ServiceCollection(); - services.AddJsonApi(opt => { - opt.BuildResourceGraph(b => { - b.AddResource("non-db-resources"); - }); - }); + services.AddJsonApi(resources: builder => builder.AddResource("non-db-resources")); - // act + // Act var container = services.BuildServiceProvider(); - // assert + // Assert var resourceGraph = container.GetRequiredService(); - var dbResource = resourceGraph.GetContextEntity("db-resources"); - var nonDbResource = resourceGraph.GetContextEntity("non-db-resources"); - Assert.Equal(typeof(DbResource), dbResource.EntityType); - Assert.Equal(typeof(NonDbResource), nonDbResource.EntityType); - Assert.Equal(typeof(ResourceDefinition), nonDbResource.ResourceType); + var dbResource = resourceGraph.GetResourceContext("db-resources"); + var nonDbResource = resourceGraph.GetResourceContext("non-db-resources"); + Assert.Equal(typeof(DbResource), dbResource.ResourceType); + Assert.Equal(typeof(NonDbResource), nonDbResource.ResourceType); + Assert.Equal(typeof(ResourceDefinition), nonDbResource.ResourceDefinitionType); } [Fact] public void Resources_Without_Names_Specified_Will_Use_Default_Formatter() { - // arrange + // Arrange var builder = new ResourceGraphBuilder(); builder.AddResource(); - // act - var graph = builder.Build(); + // Act + var resourceGraph = builder.Build(); - // assert - var resource = graph.GetContextEntity(typeof(TestResource)); - Assert.Equal("test-resources", resource.EntityName); + // Assert + var resource = resourceGraph.GetResourceContext(typeof(TestResource)); + Assert.Equal("test-resources", resource.ResourceName); } [Fact] public void Resources_Without_Names_Specified_Will_Use_Configured_Formatter() { - // arrange - JsonApiOptions.ResourceNameFormatter = new CamelCaseNameFormatter(); - var builder = new ResourceGraphBuilder(); + // Arrange + var builder = new ResourceGraphBuilder(new CamelCaseNameFormatter()); builder.AddResource(); - // act - var graph = builder.Build(); + // Act + var resourceGraph = builder.Build(); - // assert - var resource = graph.GetContextEntity(typeof(TestResource)); - Assert.Equal("testResources", resource.EntityName); + // Assert + var resource = resourceGraph.GetResourceContext(typeof(TestResource)); + Assert.Equal("testResources", resource.ResourceName); } [Fact] public void Attrs_Without_Names_Specified_Will_Use_Default_Formatter() { - // arrange + // Arrange var builder = new ResourceGraphBuilder(); builder.AddResource(); - // act - var graph = builder.Build(); + // Act + var resourceGraph = builder.Build(); - // assert - var resource = graph.GetContextEntity(typeof(TestResource)); + // Assert + var resource = resourceGraph.GetResourceContext(typeof(TestResource)); Assert.Contains(resource.Attributes, (i) => i.PublicAttributeName == "compound-attribute"); } [Fact] public void Attrs_Without_Names_Specified_Will_Use_Configured_Formatter() { - // arrange - JsonApiOptions.ResourceNameFormatter = new CamelCaseNameFormatter(); - var builder = new ResourceGraphBuilder(); + // Arrange + var builder = new ResourceGraphBuilder(new CamelCaseNameFormatter()); builder.AddResource(); - // act - var graph = builder.Build(); + // Act + var resourceGraph = builder.Build(); - // assert - var resource = graph.GetContextEntity(typeof(TestResource)); + // Assert + var resource = resourceGraph.GetResourceContext(typeof(TestResource)); Assert.Contains(resource.Attributes, (i) => i.PublicAttributeName == "compoundAttribute"); } [Fact] public void Relationships_Without_Names_Specified_Will_Use_Default_Formatter() { - // arrange + // Arrange var builder = new ResourceGraphBuilder(); builder.AddResource(); - // act - var graph = builder.Build(); + // Act + var resourceGraph = builder.Build(); - // assert - var resource = graph.GetContextEntity(typeof(TestResource)); + // Assert + var resource = resourceGraph.GetResourceContext(typeof(TestResource)); Assert.Equal("related-resource", resource.Relationships.Single(r => r.IsHasOne).PublicRelationshipName); Assert.Equal("related-resources", resource.Relationships.Single(r => r.IsHasMany).PublicRelationshipName); } - public class TestResource : Identifiable { + public class TestResource : Identifiable + { [Attr] public string CompoundAttribute { get; set; } [HasOne] public RelatedResource RelatedResource { get; set; } [HasMany] public List RelatedResources { get; set; } diff --git a/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs b/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs index 3c5e2e5147..ce6d4bf742 100644 --- a/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs +++ b/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs @@ -1,70 +1,70 @@ -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Services; -using Microsoft.AspNetCore.Http; -using Moq; -using Xunit; +//using JsonApiDotNetCore.Builders; +//using JsonApiDotNetCore.Configuration; +//using JsonApiDotNetCore.Services; +//using Microsoft.AspNetCore.Http; +//using Moq; +//using Xunit; -namespace UnitTests.Builders -{ - public class DocumentBuilderBehaviour_Tests - { +//namespace UnitTests.Builders +//{ +// public class BaseDocumentBuilderBehaviour_Tests +// { - [Theory] - [InlineData(null, null, null, false)] - [InlineData(false, null, null, false)] - [InlineData(true, null, null, true)] - [InlineData(false, false, "true", false)] - [InlineData(false, true, "true", true)] - [InlineData(true, true, "false", false)] - [InlineData(true, false, "false", true)] - [InlineData(null, false, "false", false)] - [InlineData(null, false, "true", false)] - [InlineData(null, true, "true", true)] - [InlineData(null, true, "false", false)] - [InlineData(null, true, "foo", false)] - [InlineData(null, false, "foo", false)] - [InlineData(true, true, "foo", true)] - [InlineData(true, false, "foo", true)] - [InlineData(null, true, null, false)] - [InlineData(null, false, null, false)] - public void CheckNullBehaviorCombination(bool? omitNullValuedAttributes, bool? allowClientOverride, string clientOverride, bool omitsNulls) - { +// [Theory] +// [InlineData(null, null, null, false)] +// [InlineData(false, null, null, false)] +// [InlineData(true, null, null, true)] +// [InlineData(false, false, "true", false)] +// [InlineData(false, true, "true", true)] +// [InlineData(true, true, "false", false)] +// [InlineData(true, false, "false", true)] +// [InlineData(null, false, "false", false)] +// [InlineData(null, false, "true", false)] +// [InlineData(null, true, "true", true)] +// [InlineData(null, true, "false", false)] +// [InlineData(null, true, "foo", false)] +// [InlineData(null, false, "foo", false)] +// [InlineData(true, true, "foo", true)] +// [InlineData(true, false, "foo", true)] +// [InlineData(null, true, null, false)] +// [InlineData(null, false, null, false)] +// public void CheckNullBehaviorCombination(bool? omitNullValuedAttributes, bool? allowClientOverride, string clientOverride, bool omitsNulls) +// { - NullAttributeResponseBehavior nullAttributeResponseBehavior; - if (omitNullValuedAttributes.HasValue && allowClientOverride.HasValue) - { - nullAttributeResponseBehavior = new NullAttributeResponseBehavior(omitNullValuedAttributes.Value, allowClientOverride.Value); - }else if (omitNullValuedAttributes.HasValue) - { - nullAttributeResponseBehavior = new NullAttributeResponseBehavior(omitNullValuedAttributes.Value); - }else if - (allowClientOverride.HasValue) - { - nullAttributeResponseBehavior = new NullAttributeResponseBehavior(allowClientOverride: allowClientOverride.Value); - } - else - { - nullAttributeResponseBehavior = new NullAttributeResponseBehavior(); - } +// NullAttributeResponseBehavior nullAttributeResponseBehavior; +// if (omitNullValuedAttributes.HasValue && allowClientOverride.HasValue) +// { +// nullAttributeResponseBehavior = new NullAttributeResponseBehavior(omitNullValuedAttributes.Value, allowClientOverride.Value); +// }else if (omitNullValuedAttributes.HasValue) +// { +// nullAttributeResponseBehavior = new NullAttributeResponseBehavior(omitNullValuedAttributes.Value); +// }else if +// (allowClientOverride.HasValue) +// { +// nullAttributeResponseBehavior = new NullAttributeResponseBehavior(allowClientOverride: allowClientOverride.Value); +// } +// else +// { +// nullAttributeResponseBehavior = new NullAttributeResponseBehavior(); +// } - var jsonApiContextMock = new Mock(); - jsonApiContextMock.SetupGet(m => m.Options) - .Returns(new JsonApiOptions() {NullAttributeResponseBehavior = nullAttributeResponseBehavior}); +// var jsonApiContextMock = new Mock(); +// jsonApiContextMock.SetupGet(m => m.Options) +// .Returns(new JsonApiOptions() {NullAttributeResponseBehavior = nullAttributeResponseBehavior}); - var httpContext = new DefaultHttpContext(); - if (clientOverride != null) - { - httpContext.Request.QueryString = new QueryString($"?omitNullValuedAttributes={clientOverride}"); - } - var httpContextAccessorMock = new Mock(); - httpContextAccessorMock.SetupGet(m => m.HttpContext).Returns(httpContext); +// var httpContext = new DefaultHttpContext(); +// if (clientOverride != null) +// { +// httpContext.Request.QueryString = new QueryString($"?omitNullValuedAttributes={clientOverride}"); +// } +// var httpContextAccessorMock = new Mock(); +// httpContextAccessorMock.SetupGet(m => m.HttpContext).Returns(httpContext); - var sut = new DocumentBuilderOptionsProvider(jsonApiContextMock.Object, httpContextAccessorMock.Object); - var documentBuilderOptions = sut.GetDocumentBuilderOptions(); +// var sut = new BaseDocumentBuilderOptionsProvider(jsonApiContextMock.Object, httpContextAccessorMock.Object); +// var documentBuilderOptions = sut.GetBaseDocumentBuilderOptions(); - Assert.Equal(omitsNulls, documentBuilderOptions.OmitNullValuedAttributes); - } +// Assert.Equal(omitsNulls, documentBuilderOptions.OmitNullValuedAttributes); +// } - } -} +// } +//} diff --git a/test/UnitTests/Builders/DocumentBuilder_Tests.cs b/test/UnitTests/Builders/DocumentBuilder_Tests.cs deleted file mode 100644 index 00b2cad9a0..0000000000 --- a/test/UnitTests/Builders/DocumentBuilder_Tests.cs +++ /dev/null @@ -1,411 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using Xunit; - -namespace UnitTests -{ - public class DocumentBuilder_Tests - { - private readonly Mock _jsonApiContextMock; - private readonly PageManager _pageManager; - private readonly JsonApiOptions _options; - private readonly Mock _requestMetaMock; - - public DocumentBuilder_Tests() - { - _jsonApiContextMock = new Mock(); - _requestMetaMock = new Mock(); - - _options = new JsonApiOptions(); - - _options.BuildResourceGraph(builder => - { - builder.AddResource("models"); - builder.AddResource("related-models"); - }); - - _jsonApiContextMock - .Setup(m => m.Options) - .Returns(_options); - - _jsonApiContextMock - .Setup(m => m.ResourceGraph) - .Returns(_options.ResourceGraph); - - _jsonApiContextMock - .Setup(m => m.MetaBuilder) - .Returns(new MetaBuilder()); - - _pageManager = new PageManager(); - _jsonApiContextMock - .Setup(m => m.PageManager) - .Returns(_pageManager); - - _jsonApiContextMock - .Setup(m => m.BasePath) - .Returns("localhost"); - - _jsonApiContextMock - .Setup(m => m.RequestEntity) - .Returns(_options.ResourceGraph.GetContextEntity(typeof(Model))); - } - - [Fact] - public void Includes_Paging_Links_By_Default() - { - // arrange - _pageManager.PageSize = 1; - _pageManager.TotalRecords = 1; - _pageManager.CurrentPage = 1; - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - var entity = new Model(); - - // act - var document = documentBuilder.Build(entity); - - // assert - Assert.NotNull(document.Links); - Assert.NotNull(document.Links.Last); - } - - [Fact] - public void Page_Links_Can_Be_Disabled_Globally() - { - // arrange - _pageManager.PageSize = 1; - _pageManager.TotalRecords = 1; - _pageManager.CurrentPage = 1; - - _options.BuildResourceGraph(builder => builder.DocumentLinks = Link.None); - - _jsonApiContextMock - .Setup(m => m.ResourceGraph) - .Returns(_options.ResourceGraph); - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - var entity = new Model(); - - // act - var document = documentBuilder.Build(entity); - - // assert - Assert.Null(document.Links); - } - - [Fact] - public void Related_Links_Can_Be_Disabled() - { - // arrange - _pageManager.PageSize = 1; - _pageManager.TotalRecords = 1; - _pageManager.CurrentPage = 1; - - _jsonApiContextMock - .Setup(m => m.ResourceGraph) - .Returns(_options.ResourceGraph); - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - var entity = new Model(); - - // act - var document = documentBuilder.Build(entity); - - // assert - Assert.Null(document.Data.Relationships["related-model"].Links); - } - - [Fact] - public void Related_Links_Can_Be_Disabled_Globally() - { - // arrange - _pageManager.PageSize = 1; - _pageManager.TotalRecords = 1; - _pageManager.CurrentPage = 1; - - _options.DefaultRelationshipLinks = Link.None; - - _jsonApiContextMock - .Setup(m => m.ResourceGraph) - .Returns(_options.ResourceGraph); - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - var entity = new RelatedModel(); - - // act - var document = documentBuilder.Build(entity); - - // assert - Assert.Null(document.Data.Relationships["models"].Links); - } - - [Fact] - public void Related_Data_Included_In_Relationships_By_Default() - { - // arrange - const string relatedTypeName = "related-models"; - const string relationshipName = "related-model"; - const int relatedId = 1; - _jsonApiContextMock - .Setup(m => m.ResourceGraph) - .Returns(_options.ResourceGraph); - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - var entity = new Model - { - RelatedModel = new RelatedModel - { - Id = relatedId - } - }; - - // act - var document = documentBuilder.Build(entity); - - // assert - var relationshipData = document.Data.Relationships[relationshipName]; - Assert.NotNull(relationshipData); - Assert.NotNull(relationshipData.SingleData); - Assert.NotNull(relationshipData.SingleData); - Assert.Equal(relatedId.ToString(), relationshipData.SingleData.Id); - Assert.Equal(relatedTypeName, relationshipData.SingleData.Type); - } - - [Fact] - public void IndependentIdentifier_Included_In_HasOne_Relationships_By_Default() - { - // arrange - const string relatedTypeName = "related-models"; - const string relationshipName = "related-model"; - const int relatedId = 1; - _jsonApiContextMock - .Setup(m => m.ResourceGraph) - .Returns(_options.ResourceGraph); - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - var entity = new Model - { - RelatedModelId = relatedId - }; - - // act - var document = documentBuilder.Build(entity); - - // assert - var relationshipData = document.Data.Relationships[relationshipName]; - Assert.NotNull(relationshipData); - Assert.NotNull(relationshipData.SingleData); - Assert.NotNull(relationshipData.SingleData); - Assert.Equal(relatedId.ToString(), relationshipData.SingleData.Id); - Assert.Equal(relatedTypeName, relationshipData.SingleData.Type); - } - - [Fact] - public void Build_Can_Build_Arrays() - { - var entities = new[] { new Model() }; - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - - var documents = documentBuilder.Build(entities); - - Assert.Single(documents.Data); - } - - [Fact] - public void Build_Can_Build_CustomIEnumerables() - { - var entities = new Models(new[] { new Model() }); - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); - - var documents = documentBuilder.Build(entities); - - Assert.Single(documents.Data); - } - - [Theory] - [InlineData(null, null, true)] - [InlineData(false, null, true)] - [InlineData(true, null, false)] - [InlineData(null, "foo", true)] - [InlineData(false, "foo", true)] - [InlineData(true, "foo", true)] - public void DocumentBuilderOptions( - bool? omitNullValuedAttributes, - string attributeValue, - bool resultContainsAttribute) - { - var documentBuilderBehaviourMock = new Mock(); - if (omitNullValuedAttributes.HasValue) - { - documentBuilderBehaviourMock.Setup(m => m.GetDocumentBuilderOptions()) - .Returns(new DocumentBuilderOptions(omitNullValuedAttributes.Value)); - } - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, null, omitNullValuedAttributes.HasValue ? documentBuilderBehaviourMock.Object : null); - var document = documentBuilder.Build(new Model() { StringProperty = attributeValue }); - - Assert.Equal(resultContainsAttribute, document.Data.Attributes.ContainsKey("StringProperty")); - } - - private class Model : Identifiable - { - [Attr("StringProperty")] public string StringProperty { get; set; } - - [HasOne("related-model", documentLinks: Link.None)] - public RelatedModel RelatedModel { get; set; } - public int RelatedModelId { get; set; } - } - - private class RelatedModel : Identifiable - { - [HasMany("models")] - public List Models { get; set; } - } - - private class Models : IEnumerable - { - private readonly IEnumerable models; - - public Models(IEnumerable models) - { - this.models = models; - } - - public IEnumerator GetEnumerator() - { - return models.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return models.GetEnumerator(); - } - } - - [Fact] - public void Build_Will_Use_Resource_If_Defined_For_Multiple_Documents() - { - var entities = new[] { new User() }; - var resourceGraph = new ResourceGraphBuilder() - .AddResource("user") - .Build(); - _jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - - var scopedServiceProvider = new TestScopedServiceProvider( - new ServiceCollection() - .AddScoped, UserResource>() - .AddSingleton(resourceGraph) - .BuildServiceProvider()); - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, scopedServiceProvider: scopedServiceProvider); - - var documents = documentBuilder.Build(entities); - - Assert.Single(documents.Data); - Assert.False(documents.Data[0].Attributes.ContainsKey("password")); - Assert.True(documents.Data[0].Attributes.ContainsKey("username")); - } - - [Fact] - public void Build_Will_Use_Resource_If_Defined_For_Single_Document() - { - var entity = new User(); - var resourceGraph = new ResourceGraphBuilder() - .AddResource("user") - .Build(); - _jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - - var scopedServiceProvider = new TestScopedServiceProvider( - new ServiceCollection() - .AddScoped, UserResource>() - .AddSingleton(resourceGraph) - .BuildServiceProvider()); - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, scopedServiceProvider: scopedServiceProvider); - - var documents = documentBuilder.Build(entity); - - Assert.False(documents.Data.Attributes.ContainsKey("password")); - Assert.True(documents.Data.Attributes.ContainsKey("username")); - } - - [Fact] - public void Build_Will_Use_Instance_Specific_Resource_If_Defined_For_Multiple_Documents() - { - var entities = new[] { new User() }; - var resourceGraph = new ResourceGraphBuilder() - .AddResource("user") - .Build(); - _jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - - var scopedServiceProvider = new TestScopedServiceProvider( - new ServiceCollection() - .AddScoped, InstanceSpecificUserResource>() - .AddSingleton(resourceGraph) - .BuildServiceProvider()); - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, scopedServiceProvider: scopedServiceProvider); - - var documents = documentBuilder.Build(entities); - - Assert.Single(documents.Data); - Assert.False(documents.Data[0].Attributes.ContainsKey("password")); - Assert.True(documents.Data[0].Attributes.ContainsKey("username")); - } - - [Fact] - public void Build_Will_Use_Instance_Specific_Resource_If_Defined_For_Single_Document() - { - var entity = new User(); - var resourceGraph = new ResourceGraphBuilder() - .AddResource("user") - .Build(); - _jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - - var scopedServiceProvider = new TestScopedServiceProvider( - new ServiceCollection() - .AddScoped, InstanceSpecificUserResource>() - .AddSingleton(resourceGraph) - .BuildServiceProvider()); - - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, scopedServiceProvider: scopedServiceProvider); - - var documents = documentBuilder.Build(entity); - - Assert.False(documents.Data.Attributes.ContainsKey("password")); - Assert.True(documents.Data.Attributes.ContainsKey("username")); - } - - public class User : Identifiable - { - [Attr("username")] public string Username { get; set; } - [Attr("password")] public string Password { get; set; } - } - - public class InstanceSpecificUserResource : ResourceDefinition - { - public InstanceSpecificUserResource(IResourceGraph graph) : base(graph) - { - } - - protected override List OutputAttrs(User instance) - => Remove(user => user.Password); - } - - public class UserResource : ResourceDefinition - { - public UserResource(IResourceGraph graph) : base(graph) - { - } - - protected override List OutputAttrs() - => Remove(user => user.Password); - } - } -} diff --git a/test/UnitTests/Builders/LinkBuilderTests.cs b/test/UnitTests/Builders/LinkBuilderTests.cs new file mode 100644 index 0000000000..0282f52527 --- /dev/null +++ b/test/UnitTests/Builders/LinkBuilderTests.cs @@ -0,0 +1,218 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Models.Links; +using JsonApiDotNetCoreExample.Models; +using Moq; +using Xunit; +using JsonApiDotNetCore.Query; +using JsonApiDotNetCore.Serialization.Server.Builders; + +namespace UnitTests +{ + public class LinkBuilderTests + { + private readonly IPageService _pageService; + private readonly Mock _provider = new Mock(); + private const string _host = "http://www.example.com"; + private const string _topSelf = "http://www.example.com/articles"; + private const string _resourceSelf = "http://www.example.com/articles/123"; + private const string _relSelf = "http://www.example.com/articles/123/relationships/author"; + private const string _relRelated = "http://www.example.com/articles/123/author"; + + public LinkBuilderTests() + { + _pageService = GetPageManager(); + } + + [Theory] + [InlineData(Link.All, Link.NotConfigured, _resourceSelf)] + [InlineData(Link.Self, Link.NotConfigured, _resourceSelf)] + [InlineData(Link.None, Link.NotConfigured, null)] + [InlineData(Link.All, Link.Self, _resourceSelf)] + [InlineData(Link.Self, Link.Self, _resourceSelf)] + [InlineData(Link.None, Link.Self, _resourceSelf)] + [InlineData(Link.All, Link.None, null)] + [InlineData(Link.Self, Link.None, null)] + [InlineData(Link.None, Link.None, null)] + public void BuildResourceLinks_GlobalAndResourceConfiguration_ExpectedResult(Link global, Link resource, object expectedResult) + { + // Arrange + var config = GetConfiguration(resourceLinks: global); + var primaryResource = GetResourceContext
(resourceLinks: resource); + _provider.Setup(m => m.GetResourceContext("articles")).Returns(primaryResource); + var builder = new LinkBuilder(config, GetRequestManager(), null, _provider.Object); + + // Act + var links = builder.GetResourceLinks("articles", "123"); + + // Assert + if (expectedResult == null) + Assert.Null(links); + else + Assert.Equal(_resourceSelf, links.Self); + } + + [Theory] + [InlineData(Link.All, Link.NotConfigured, Link.NotConfigured, _relSelf, _relRelated)] + [InlineData(Link.All, Link.NotConfigured, Link.All, _relSelf, _relRelated)] + [InlineData(Link.All, Link.NotConfigured, Link.Self, _relSelf, null)] + [InlineData(Link.All, Link.NotConfigured, Link.Related, null, _relRelated)] + [InlineData(Link.All, Link.NotConfigured, Link.None, null, null)] + [InlineData(Link.All, Link.All, Link.NotConfigured, _relSelf, _relRelated)] + [InlineData(Link.All, Link.All, Link.All, _relSelf, _relRelated)] + [InlineData(Link.All, Link.All, Link.Self, _relSelf, null)] + [InlineData(Link.All, Link.All, Link.Related, null, _relRelated)] + [InlineData(Link.All, Link.All, Link.None, null, null)] + [InlineData(Link.All, Link.Self, Link.NotConfigured, _relSelf, null)] + [InlineData(Link.All, Link.Self, Link.All, _relSelf, _relRelated)] + [InlineData(Link.All, Link.Self, Link.Self, _relSelf, null)] + [InlineData(Link.All, Link.Self, Link.Related, null, _relRelated)] + [InlineData(Link.All, Link.Self, Link.None, null, null)] + [InlineData(Link.All, Link.Related, Link.NotConfigured, null, _relRelated)] + [InlineData(Link.All, Link.Related, Link.All, _relSelf, _relRelated)] + [InlineData(Link.All, Link.Related, Link.Self, _relSelf, null)] + [InlineData(Link.All, Link.Related, Link.Related, null, _relRelated)] + [InlineData(Link.All, Link.Related, Link.None, null, null)] + [InlineData(Link.All, Link.None, Link.NotConfigured, null, null)] + [InlineData(Link.All, Link.None, Link.All, _relSelf, _relRelated)] + [InlineData(Link.All, Link.None, Link.Self, _relSelf, null)] + [InlineData(Link.All, Link.None, Link.Related, null, _relRelated)] + [InlineData(Link.All, Link.None, Link.None, null, null)] + public void BuildRelationshipLinks_GlobalResourceAndAttrConfiguration_ExpectedLinks(Link global, + Link resource, + Link relationship, + object expectedSelfLink, + object expectedRelatedLink) + { + // Arrange + var config = GetConfiguration(relationshipLinks: global); + var primaryResource = GetResourceContext
(relationshipLinks: resource); + _provider.Setup(m => m.GetResourceContext(typeof(Article))).Returns(primaryResource); + var builder = new LinkBuilder(config, GetRequestManager(), null, _provider.Object); + var attr = new HasOneAttribute(links: relationship) { RightType = typeof(Author), PublicRelationshipName = "author" }; + + // Act + var links = builder.GetRelationshipLinks(attr, new Article { Id = 123 }); + + // Assert + if (expectedSelfLink == null && expectedRelatedLink == null) + { + Assert.Null(links); + } + else + { + Assert.Equal(expectedSelfLink, links.Self); + Assert.Equal(expectedRelatedLink, links.Related); + } + } + + [Theory] + [InlineData(Link.All, Link.NotConfigured, _topSelf, true)] + [InlineData(Link.All, Link.All, _topSelf, true)] + [InlineData(Link.All, Link.Self, _topSelf, false)] + [InlineData(Link.All, Link.Paging, null, true)] + [InlineData(Link.All, Link.None, null, false)] + [InlineData(Link.Self, Link.NotConfigured, _topSelf, false)] + [InlineData(Link.Self, Link.All, _topSelf, true)] + [InlineData(Link.Self, Link.Self, _topSelf, false)] + [InlineData(Link.Self, Link.Paging, null, true)] + [InlineData(Link.Self, Link.None, null, false)] + [InlineData(Link.Paging, Link.NotConfigured, null, true)] + [InlineData(Link.Paging, Link.All, _topSelf, true)] + [InlineData(Link.Paging, Link.Self, _topSelf, false)] + [InlineData(Link.Paging, Link.Paging, null, true)] + [InlineData(Link.Paging, Link.None, null, false)] + [InlineData(Link.None, Link.NotConfigured, null, false)] + [InlineData(Link.None, Link.All, _topSelf, true)] + [InlineData(Link.None, Link.Self, _topSelf, false)] + [InlineData(Link.None, Link.Paging, null, true)] + [InlineData(Link.None, Link.None, null, false)] + public void BuildTopLevelLinks_GlobalAndResourceConfiguration_ExpectedLinks(Link global, + Link resource, + object expectedSelfLink, + bool pages) + { + // Arrange + var config = GetConfiguration(topLevelLinks: global); + var primaryResource = GetResourceContext
(topLevelLinks: resource); + _provider.Setup(m => m.GetResourceContext
()).Returns(primaryResource); + + var builder = new LinkBuilder(config, GetRequestManager(), _pageService, _provider.Object); + + // Act + var links = builder.GetTopLevelLinks(primaryResource); + + // Assert + if (!pages && expectedSelfLink == null) + { + Assert.Null(links); + } + else + { + Assert.Equal(expectedSelfLink, links.Self); + Assert.True(CheckPages(links, pages)); + } + } + + private bool CheckPages(TopLevelLinks links, bool pages) + { + if (pages) + { + return links.First == $"{_host}/articles?page[size]=10&page[number]=1" + && links.Prev == $"{_host}/articles?page[size]=10&page[number]=1" + && links.Next == $"{_host}/articles?page[size]=10&page[number]=3" + && links.Last == $"{_host}/articles?page[size]=10&page[number]=3"; + } + return links.First == null && links.Prev == null && links.Next == null && links.Last == null; + } + + private ICurrentRequest GetRequestManager(ResourceContext resourceContext = null) + { + var mock = new Mock(); + mock.Setup(m => m.BasePath).Returns(_host); + mock.Setup(m => m.GetRequestResource()).Returns(resourceContext); + return mock.Object; + } + + private ILinksConfiguration GetConfiguration(Link resourceLinks = Link.All, + Link topLevelLinks = Link.All, + Link relationshipLinks = Link.All) + { + var config = new Mock(); + config.Setup(m => m.TopLevelLinks).Returns(topLevelLinks); + config.Setup(m => m.ResourceLinks).Returns(resourceLinks); + config.Setup(m => m.RelationshipLinks).Returns(relationshipLinks); + return config.Object; + } + + private IPageService GetPageManager() + { + var mock = new Mock(); + mock.Setup(m => m.ShouldPaginate()).Returns(true); + mock.Setup(m => m.CurrentPage).Returns(2); + mock.Setup(m => m.TotalPages).Returns(3); + mock.Setup(m => m.PageSize).Returns(10); + return mock.Object; + + } + + + + private ResourceContext GetResourceContext(Link resourceLinks = Link.NotConfigured, + Link topLevelLinks = Link.NotConfigured, + Link relationshipLinks = Link.NotConfigured) where TResource : class, IIdentifiable + { + return new ResourceContext + { + ResourceLinks = resourceLinks, + TopLevelLinks = topLevelLinks, + RelationshipLinks = relationshipLinks, + ResourceName = typeof(TResource).Name.Dasherize() + "s" + }; + } + } +} diff --git a/test/UnitTests/Builders/LinkBuilder_Tests.cs b/test/UnitTests/Builders/LinkBuilder_Tests.cs deleted file mode 100644 index 69b135de03..0000000000 --- a/test/UnitTests/Builders/LinkBuilder_Tests.cs +++ /dev/null @@ -1,48 +0,0 @@ -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Services; -using Microsoft.AspNetCore.Http; -using Moq; -using Xunit; - -namespace UnitTests -{ - public class LinkBuilder_Tests - { - [Theory] - [InlineData("http", "localhost", "/api/v1/articles", false, "http://localhost/api/v1")] - [InlineData("https", "localhost", "/api/v1/articles", false, "https://localhost/api/v1")] - [InlineData("http", "example.com", "/api/v1/articles", false, "http://example.com/api/v1")] - [InlineData("https", "example.com", "/api/v1/articles", false, "https://example.com/api/v1")] - [InlineData("https", "example.com", "/articles", false, "https://example.com")] - [InlineData("https", "example.com", "/articles", true, "")] - [InlineData("https", "example.com", "/api/v1/articles", true, "/api/v1")] - public void GetBasePath_Returns_Path_Before_Resource(string scheme, - string host, string path, bool isRelative, string expectedPath) - { - // arrange - const string resource = "articles"; - var jsonApiContextMock = new Mock(); - jsonApiContextMock.Setup(m => m.Options).Returns(new JsonApiOptions - { - RelativeLinks = isRelative - }); - - var requestMock = new Mock(); - requestMock.Setup(m => m.Scheme).Returns(scheme); - requestMock.Setup(m => m.Host).Returns(new HostString(host)); - requestMock.Setup(m => m.Path).Returns(new PathString(path)); - - var contextMock = new Mock(); - contextMock.Setup(m => m.Request).Returns(requestMock.Object); - - var linkBuilder = new LinkBuilder(jsonApiContextMock.Object); - - // act - var basePath = linkBuilder.GetBasePath(contextMock.Object, resource); - - // assert - Assert.Equal(expectedPath, basePath); - } - } -} diff --git a/test/UnitTests/Builders/LinkTests.cs b/test/UnitTests/Builders/LinkTests.cs new file mode 100644 index 0000000000..96d49b2f22 --- /dev/null +++ b/test/UnitTests/Builders/LinkTests.cs @@ -0,0 +1,38 @@ +using JsonApiDotNetCore.Models.Links; +using Xunit; + +namespace UnitTests.Builders +{ + public class LinkTests + { + [Theory] + [InlineData(Link.All, Link.Self, true)] + [InlineData(Link.All, Link.Related, true)] + [InlineData(Link.All, Link.Paging, true)] + [InlineData(Link.None, Link.Self, false)] + [InlineData(Link.None, Link.Related, false)] + [InlineData(Link.None, Link.Paging, false)] + [InlineData(Link.NotConfigured, Link.Self, false)] + [InlineData(Link.NotConfigured, Link.Related, false)] + [InlineData(Link.NotConfigured, Link.Paging, false)] + [InlineData(Link.Self, Link.Self, true)] + [InlineData(Link.Self, Link.Related, false)] + [InlineData(Link.Self, Link.Paging, false)] + [InlineData(Link.Self, Link.None, false)] + [InlineData(Link.Self, Link.NotConfigured, false)] + [InlineData(Link.Related, Link.Self, false)] + [InlineData(Link.Related, Link.Related, true)] + [InlineData(Link.Related, Link.Paging, false)] + [InlineData(Link.Related, Link.None, false)] + [InlineData(Link.Related, Link.NotConfigured, false)] + [InlineData(Link.Paging, Link.Self, false)] + [InlineData(Link.Paging, Link.Related, false)] + [InlineData(Link.Paging, Link.Paging, true)] + [InlineData(Link.Paging, Link.None, false)] + [InlineData(Link.Paging, Link.NotConfigured, false)] + public void LinkHasFlag_BaseLinkAndCheckLink_ExpectedResult(Link baseLink, Link checkLink, bool equal) + { + Assert.Equal(equal, baseLink.HasFlag(checkLink)); + } + } +} diff --git a/test/UnitTests/Builders/MetaBuilderTests.cs b/test/UnitTests/Builders/MetaBuilderTests.cs index 0b784ef5b7..58e35b7bd6 100644 --- a/test/UnitTests/Builders/MetaBuilderTests.cs +++ b/test/UnitTests/Builders/MetaBuilderTests.cs @@ -1,73 +1,73 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Builders; -using Xunit; +//using System.Collections.Generic; +//using JsonApiDotNetCore.Builders; +//using Xunit; -namespace UnitTests.Builders -{ - public class MetaBuilderTests - { - [Fact] - public void Can_Add_Key_Value() - { - // arrange - var builder = new MetaBuilder(); - var key = "test"; - var value = "testValue"; +//namespace UnitTests.Builders +//{ +// public class MetaBuilderTests +// { +// [Fact] +// public void Can_Add_Key_Value() +// { +// // Arrange +// var builder = new MetaBuilder(); +// var key = "test"; +// var value = "testValue"; - // act - builder.Add(key, value); - var result = builder.Build(); +// // Act +// builder.Add(key, value); +// var result = builder.Build(); - // assert - Assert.NotEmpty(result); - Assert.Equal(value, result[key]); - } +// // Assert +// Assert.NotEmpty(result); +// Assert.Equal(value, result[key]); +// } - [Fact] - public void Can_Add_Multiple_Values() - { - // arrange - var builder = new MetaBuilder(); - var input = new Dictionary { - { "key1", "value1" }, - { "key2", "value2" } - }; +// [Fact] +// public void Can_Add_Multiple_Values() +// { +// // Arrange +// var builder = new MetaBuilder(); +// var input = new Dictionary { +// { "key1", "value1" }, +// { "key2", "value2" } +// }; - // act - builder.Add(input); - var result = builder.Build(); +// // Act +// builder.Add(input); +// var result = builder.Build(); - // assert - Assert.NotEmpty(result); - foreach (var entry in input) - Assert.Equal(input[entry.Key], result[entry.Key]); - } +// // Assert +// Assert.NotEmpty(result); +// foreach (var entry in input) +// Assert.Equal(input[entry.Key], result[entry.Key]); +// } - [Fact] - public void When_Adding_Duplicate_Values_Keep_Newest() - { - // arrange - var builder = new MetaBuilder(); +// [Fact] +// public void When_Adding_Duplicate_Values_Keep_Newest() +// { +// // Arrange +// var builder = new MetaBuilder(); - var key = "key"; - var oldValue = "oldValue"; - var newValue = "newValue"; +// var key = "key"; +// var oldValue = "oldValue"; +// var newValue = "newValue"; - builder.Add(key, oldValue); +// builder.Add(key, oldValue); - var input = new Dictionary { - { key, newValue }, - { "key2", "value2" } - }; +// var input = new Dictionary { +// { key, newValue }, +// { "key2", "value2" } +// }; - // act - builder.Add(input); - var result = builder.Build(); +// // Act +// builder.Add(input); +// var result = builder.Build(); - // assert - Assert.NotEmpty(result); - Assert.Equal(input.Count, result.Count); - Assert.Equal(input[key], result[key]); - } - } -} +// // Assert +// Assert.NotEmpty(result); +// Assert.Equal(input.Count, result.Count); +// Assert.Equal(input[key], result[key]); +// } +// } +//} diff --git a/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs b/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs index 3397aa4eda..d33ba1da04 100644 --- a/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs +++ b/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; + namespace UnitTests { public class BaseJsonApiController_Tests @@ -17,266 +18,249 @@ public class Resource : Identifiable { [Attr("test-attribute")] public string TestAttribute { get; set; } } - private Mock _jsonApiContextMock = new Mock(); - private Mock _resourceGraphMock = new Mock(); [Fact] public async Task GetAsync_Calls_Service() { - // arrange + // Arrange var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getAll: serviceMock.Object); + var controller = new BaseJsonApiController(new Mock().Object, getAll: serviceMock.Object); - // act + // Act await controller.GetAsync(); - // assert + // Assert serviceMock.Verify(m => m.GetAsync(), Times.Once); - VerifyApplyContext(); + } [Fact] public async Task GetAsync_Throws_405_If_No_Service() { - // arrange + // Arrange var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, null); + var controller = new BaseJsonApiController(new Mock().Object, null); - // act + // Act var exception = await Assert.ThrowsAsync(() => controller.GetAsync()); - // assert + // Assert Assert.Equal(405, exception.GetStatusCode()); } [Fact] public async Task GetAsyncById_Calls_Service() { - // arrange + // Arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getById: serviceMock.Object); + var controller = new BaseJsonApiController(new Mock().Object, getById: serviceMock.Object); - // act + // Act await controller.GetAsync(id); - // assert + // Assert serviceMock.Verify(m => m.GetAsync(id), Times.Once); - VerifyApplyContext(); + } [Fact] public async Task GetAsyncById_Throws_405_If_No_Service() { - // arrange + // Arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getById: null); + var controller = new BaseJsonApiController(new Mock().Object, getById: null); - // act + // Act var exception = await Assert.ThrowsAsync(() => controller.GetAsync(id)); - // assert + // Assert Assert.Equal(405, exception.GetStatusCode()); } [Fact] public async Task GetRelationshipsAsync_Calls_Service() { - // arrange + // Arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationships: serviceMock.Object); + var controller = new BaseJsonApiController(new Mock().Object, getRelationships: serviceMock.Object); - // act + // Act await controller.GetRelationshipsAsync(id, string.Empty); - // assert + // Assert serviceMock.Verify(m => m.GetRelationshipsAsync(id, string.Empty), Times.Once); - VerifyApplyContext(); } [Fact] public async Task GetRelationshipsAsync_Throws_405_If_No_Service() { - // arrange + // Arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationships: null); + var controller = new BaseJsonApiController(new Mock().Object, getRelationships: null); - // act + // Act var exception = await Assert.ThrowsAsync(() => controller.GetRelationshipsAsync(id, string.Empty)); - // assert + // Assert Assert.Equal(405, exception.GetStatusCode()); } [Fact] public async Task GetRelationshipAsync_Calls_Service() { - // arrange + // Arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationship: serviceMock.Object); + var controller = new BaseJsonApiController(new Mock().Object, getRelationship: serviceMock.Object); - // act + // Act await controller.GetRelationshipAsync(id, string.Empty); - // assert + // Assert serviceMock.Verify(m => m.GetRelationshipAsync(id, string.Empty), Times.Once); - VerifyApplyContext(); } [Fact] public async Task GetRelationshipAsync_Throws_405_If_No_Service() { - // arrange + // Arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationship: null); + var controller = new BaseJsonApiController(new Mock().Object, getRelationship: null); - // act + // Act var exception = await Assert.ThrowsAsync(() => controller.GetRelationshipAsync(id, string.Empty)); - // assert + // Assert Assert.Equal(405, exception.GetStatusCode()); } [Fact] public async Task PatchAsync_Calls_Service() { - // arrange + // Arrange const int id = 0; var resource = new Resource(); var serviceMock = new Mock>(); - _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); - _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions()); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: serviceMock.Object); - // act + var controller = new BaseJsonApiController(new JsonApiOptions(), update: serviceMock.Object); + + // Act await controller.PatchAsync(id, resource); - // assert + // Assert serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny()), Times.Once); - VerifyApplyContext(); } [Fact] public async Task PatchAsync_ModelStateInvalid_ValidateModelStateDisbled() { - // arrange + // Arrange const int id = 0; var resource = new Resource(); var serviceMock = new Mock>(); - _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); - _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = false }); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: serviceMock.Object); + var controller = new BaseJsonApiController(new JsonApiOptions(), update: serviceMock.Object); - // act + // Act var response = await controller.PatchAsync(id, resource); - // assert + // Assert serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny()), Times.Once); - VerifyApplyContext(); Assert.IsNotType(response); } [Fact] public async Task PatchAsync_ModelStateInvalid_ValidateModelStateEnabled() { - // arrange + // Arrange const int id = 0; var resource = new Resource(); var serviceMock = new Mock>(); - _jsonApiContextMock.SetupGet(a => a.ResourceGraph).Returns(_resourceGraphMock.Object); - _resourceGraphMock.Setup(a => a.GetPublicAttributeName("TestAttribute")).Returns("test-attribute"); - _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); - _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions{ValidateModelState = true}); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: serviceMock.Object); + + var controller = new BaseJsonApiController(new JsonApiOptions { ValidateModelState = true }, update: serviceMock.Object); controller.ModelState.AddModelError("TestAttribute", "Failed Validation"); - // act + // Act var response = await controller.PatchAsync(id, resource); - // assert + // Assert serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny()), Times.Never); Assert.IsType(response); - Assert.IsType(((UnprocessableEntityObjectResult) response).Value); + Assert.IsType(((UnprocessableEntityObjectResult)response).Value); } [Fact] public async Task PatchAsync_Throws_405_If_No_Service() { - // arrange + // Arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: null); + var controller = new BaseJsonApiController(new Mock().Object, update: null); - // act + // Act var exception = await Assert.ThrowsAsync(() => controller.PatchAsync(id, It.IsAny())); - // assert + // Assert Assert.Equal(405, exception.GetStatusCode()); } [Fact] public async Task PostAsync_Calls_Service() { - // arrange + // Arrange var resource = new Resource(); var serviceMock = new Mock>(); - _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); - _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions()); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, create: serviceMock.Object); + + var controller = new BaseJsonApiController(new JsonApiOptions(), create: serviceMock.Object); serviceMock.Setup(m => m.CreateAsync(It.IsAny())).ReturnsAsync(resource); - controller.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext {HttpContext = new DefaultHttpContext()}; + controller.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext { HttpContext = new DefaultHttpContext() }; - // act + // Act await controller.PostAsync(resource); - // assert + // Assert serviceMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Once); - VerifyApplyContext(); } [Fact] public async Task PostAsync_ModelStateInvalid_ValidateModelStateDisabled() { - // arrange + // Arrange var resource = new Resource(); var serviceMock = new Mock>(); - _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); - _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = false }); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, create: serviceMock.Object); - serviceMock.Setup(m => m.CreateAsync(It.IsAny())).ReturnsAsync(resource); + var controller = new BaseJsonApiController(new JsonApiOptions { ValidateModelState = false }, create: serviceMock.Object); controller.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext { HttpContext = new DefaultHttpContext() }; + serviceMock.Setup(m => m.CreateAsync(It.IsAny())).ReturnsAsync(resource); - // act + + // Act var response = await controller.PostAsync(resource); - // assert + // Assert serviceMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Once); - VerifyApplyContext(); Assert.IsNotType(response); } [Fact] public async Task PostAsync_ModelStateInvalid_ValidateModelStateEnabled() { - // arrange + // Arrange var resource = new Resource(); var serviceMock = new Mock>(); - _jsonApiContextMock.SetupGet(a => a.ResourceGraph).Returns(_resourceGraphMock.Object); - _resourceGraphMock.Setup(a => a.GetPublicAttributeName("TestAttribute")).Returns("test-attribute"); - _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); - _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = true }); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, create: serviceMock.Object); + var controller = new BaseJsonApiController(new JsonApiOptions { ValidateModelState = true }, create: serviceMock.Object); + controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }; controller.ModelState.AddModelError("TestAttribute", "Failed Validation"); + serviceMock.Setup(m => m.CreateAsync(It.IsAny())).ReturnsAsync(resource); - // act + + // Act var response = await controller.PostAsync(resource); - // assert + // Assert serviceMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Never); Assert.IsType(response); Assert.IsType(((UnprocessableEntityObjectResult)response).Value); @@ -285,68 +269,67 @@ public async Task PostAsync_ModelStateInvalid_ValidateModelStateEnabled() [Fact] public async Task PatchRelationshipsAsync_Calls_Service() { - // arrange + // Arrange const int id = 0; var resource = new Resource(); var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, updateRelationships: serviceMock.Object); + var controller = new BaseJsonApiController(new Mock().Object, updateRelationships: serviceMock.Object); - // act + // Act await controller.PatchRelationshipsAsync(id, string.Empty, null); - // assert + // Assert serviceMock.Verify(m => m.UpdateRelationshipsAsync(id, string.Empty, null), Times.Once); - VerifyApplyContext(); } [Fact] public async Task PatchRelationshipsAsync_Throws_405_If_No_Service() { - // arrange + // Arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, updateRelationships: null); + var controller = new BaseJsonApiController(new Mock().Object, updateRelationships: null); - // act + // Act var exception = await Assert.ThrowsAsync(() => controller.PatchRelationshipsAsync(id, string.Empty, null)); - // assert + // Assert Assert.Equal(405, exception.GetStatusCode()); } [Fact] public async Task DeleteAsync_Calls_Service() { - // arrange + // Arrange const int id = 0; var resource = new Resource(); var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, delete: serviceMock.Object); + var controller = new BaseJsonApiController(new Mock().Object, delete: serviceMock.Object); - // act + // Act await controller.DeleteAsync(id); - // assert + // Assert serviceMock.Verify(m => m.DeleteAsync(id), Times.Once); - VerifyApplyContext(); } [Fact] public async Task DeleteAsync_Throws_405_If_No_Service() { - // arrange + // Arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, delete: null); + var controller = new BaseJsonApiController(new Mock().Object, + + delete: null); - // act + // Act var exception = await Assert.ThrowsAsync(() => controller.DeleteAsync(id)); - // assert + // Assert Assert.Equal(405, exception.GetStatusCode()); } - private void VerifyApplyContext() - => _jsonApiContextMock.Verify(m => m.ApplyContext(It.IsAny>()), Times.Once); + } } diff --git a/test/UnitTests/Controllers/JsonApiControllerMixin_Tests.cs b/test/UnitTests/Controllers/JsonApiControllerMixin_Tests.cs index 850c459e32..00f265daed 100644 --- a/test/UnitTests/Controllers/JsonApiControllerMixin_Tests.cs +++ b/test/UnitTests/Controllers/JsonApiControllerMixin_Tests.cs @@ -1,56 +1,56 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Internal; -using Microsoft.AspNetCore.Mvc; -using Xunit; - -namespace UnitTests -{ - public class JsonApiControllerMixin_Tests : JsonApiControllerMixin - { - - [Fact] - public void Errors_Correctly_Infers_Status_Code() - { - // arrange - var errors422 = new ErrorCollection { - Errors = new List { - new Error(422, "bad specific"), - new Error(422, "bad other specific"), - } - }; - - var errors400 = new ErrorCollection { - Errors = new List { - new Error(200, "weird"), - new Error(400, "bad"), - new Error(422, "bad specific"), - } - }; - - var errors500 = new ErrorCollection { - Errors = new List { - new Error(200, "weird"), - new Error(400, "bad"), - new Error(422, "bad specific"), - new Error(500, "really bad"), - new Error(502, "really bad specific"), - } - }; - - // act - var result422 = this.Errors(errors422); - var result400 = this.Errors(errors400); - var result500 = this.Errors(errors500); - - // assert - var response422 = Assert.IsType(result422); - var response400 = Assert.IsType(result400); - var response500 = Assert.IsType(result500); - - Assert.Equal(422, response422.StatusCode); - Assert.Equal(400, response400.StatusCode); - Assert.Equal(500, response500.StatusCode); - } - } -} +using System.Collections.Generic; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal; +using Microsoft.AspNetCore.Mvc; +using Xunit; + +namespace UnitTests +{ + public class JsonApiControllerMixin_Tests : JsonApiControllerMixin + { + + [Fact] + public void Errors_Correctly_Infers_Status_Code() + { + // Arrange + var errors422 = new ErrorCollection { + Errors = new List { + new Error(422, "bad specific"), + new Error(422, "bad other specific"), + } + }; + + var errors400 = new ErrorCollection { + Errors = new List { + new Error(200, "weird"), + new Error(400, "bad"), + new Error(422, "bad specific"), + } + }; + + var errors500 = new ErrorCollection { + Errors = new List { + new Error(200, "weird"), + new Error(400, "bad"), + new Error(422, "bad specific"), + new Error(500, "really bad"), + new Error(502, "really bad specific"), + } + }; + + // Act + var result422 = this.Errors(errors422); + var result400 = this.Errors(errors400); + var result500 = this.Errors(errors500); + + // Assert + var response422 = Assert.IsType(result422); + var response400 = Assert.IsType(result400); + var response500 = Assert.IsType(result500); + + Assert.Equal(422, response422.StatusCode); + Assert.Equal(400, response400.StatusCode); + Assert.Equal(500, response500.StatusCode); + } + } +} diff --git a/test/UnitTests/Data/DefaultEntityRepositoryTest.cs b/test/UnitTests/Data/DefaultEntityRepositoryTest.cs new file mode 100644 index 0000000000..1428ab3376 --- /dev/null +++ b/test/UnitTests/Data/DefaultEntityRepositoryTest.cs @@ -0,0 +1,74 @@ +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using Microsoft.EntityFrameworkCore; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace UnitTests.Data +{ + public class DefaultEntityRepositoryTest + { + + [Fact] + public async Task PageAsync_IQueryableIsAListAndPageNumberPositive_CanStillCount() + { + // If IQueryable is actually a list (this can happen after a filter or hook) + // It needs to not do CountAsync, because well.. its not asynchronous. + + // Arrange + var repository = Setup(); + var todoItems = new List() { + new TodoItem{ Id = 1 }, + new TodoItem{ Id = 2 } + }; + + // Act + var result = await repository.PageAsync(todoItems.AsQueryable(), pageSize: 1, pageNumber: 2); + + // Assert + Assert.True(result.ElementAt(0).Id == todoItems[1].Id); + } + + [Fact] + public async Task PageAsync_IQueryableIsAListAndPageNumberNegative_CanStillCount() + { + // If IQueryable is actually a list (this can happen after a filter or hook) + // It needs to not do CountAsync, because well.. its not asynchronous. + + // Arrange + var repository = Setup(); + var todoItems = new List() { + new TodoItem{ Id = 1 }, + new TodoItem{ Id = 2 }, + new TodoItem{ Id = 3 }, + new TodoItem{ Id = 4 } + }; + + // Act + var result = await repository.PageAsync(todoItems.AsQueryable(), pageSize: 1, pageNumber: -2); + + // Assert + Assert.True(result.First().Id == 3); + } + + private DefaultResourceRepository Setup() + { + var contextResolverMock = new Mock(); + contextResolverMock.Setup(m => m.GetContext()).Returns(new Mock().Object); + var resourceGraph = new Mock(); + var targetedFields = new Mock(); + var repository = new DefaultResourceRepository(targetedFields.Object, contextResolverMock.Object, resourceGraph.Object, null); + return repository; + } + + } +} diff --git a/test/UnitTests/Data/DefaultEntityRepository_Tests.cs b/test/UnitTests/Data/DefaultEntityRepository_Tests.cs deleted file mode 100644 index 97cf51d587..0000000000 --- a/test/UnitTests/Data/DefaultEntityRepository_Tests.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System; -using System.Collections.Generic; -using JsonApiDotNetCore.Controllers; -using Xunit; -using Moq; -using Microsoft.EntityFrameworkCore; -using JsonApiDotNetCoreExample.Models; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Models; -using Microsoft.Extensions.Logging; -using JsonApiDotNetCore.Services; -using System.Threading.Tasks; -using System.Linq; -using JsonApiDotNetCore.Request; - -namespace UnitTests.Data -{ - public class DefaultEntityRepository_Tests : JsonApiControllerMixin - { - private readonly Mock _jsonApiContextMock; - private readonly Mock _loggFactoryMock; - private readonly Mock> _dbSetMock; - private readonly Mock _contextMock; - private readonly Mock _contextResolverMock; - private readonly TodoItem _todoItem; - private Dictionary _attrsToUpdate = new Dictionary(); - private Dictionary _relationshipsToUpdate = new Dictionary(); - - public DefaultEntityRepository_Tests() - { - _todoItem = new TodoItem - { - Id = 1, - Description = Guid.NewGuid().ToString(), - Ordinal = 10 - }; - _jsonApiContextMock = new Mock(); - _loggFactoryMock = new Mock(); - _dbSetMock = DbSetMock.Create(new[] { _todoItem }); - _contextMock = new Mock(); - _contextResolverMock = new Mock(); - } - - [Fact] - public async Task UpdateAsync_Updates_Attributes_In_AttributesToUpdate() - { - // arrange - var todoItemUpdates = new TodoItem - { - Id = _todoItem.Id, - Description = Guid.NewGuid().ToString() - }; - - var descAttr = new AttrAttribute("description", "Description"); - descAttr.PropertyInfo = typeof(TodoItem).GetProperty(nameof(TodoItem.Description)); - - _attrsToUpdate = new Dictionary - { - { - descAttr, - null //todoItemUpdates.Description - } - }; - - var repository = GetRepository(); - - // act - var updatedItem = await repository.UpdateAsync(todoItemUpdates); - - // assert - Assert.NotNull(updatedItem); - Assert.Equal(_todoItem.Ordinal, updatedItem.Ordinal); - Assert.Equal(todoItemUpdates.Description, updatedItem.Description); - } - - private DefaultEntityRepository GetRepository() - { - - _contextMock - .Setup(m => m.Set()) - .Returns(_dbSetMock.Object); - - _contextResolverMock - .Setup(m => m.GetContext()) - .Returns(_contextMock.Object); - - _jsonApiContextMock - .Setup(m => m.AttributesToUpdate) - .Returns(_attrsToUpdate); - - _jsonApiContextMock - .Setup(m => m.RelationshipsToUpdate) - .Returns(_relationshipsToUpdate); - - _jsonApiContextMock - .Setup(m => m.HasManyRelationshipPointers) - .Returns(new HasManyRelationshipPointers()); - - _jsonApiContextMock - .Setup(m => m.HasOneRelationshipPointers) - .Returns(new HasOneRelationshipPointers()); - - return new DefaultEntityRepository( - _loggFactoryMock.Object, - _jsonApiContextMock.Object, - _contextResolverMock.Object); - } - - [Theory] - [InlineData(0)] - [InlineData(-1)] - [InlineData(-10)] - public async Task Page_When_PageSize_Is_NonPositive_Does_Nothing(int pageSize) - { - var todoItems = DbSetMock.Create(TodoItems(2, 3, 1)).Object; - var repository = GetRepository(); - - var result = await repository.PageAsync(todoItems, pageSize, 3); - - Assert.Equal(TodoItems(2, 3, 1), result, new IdComparer()); - } - - [Fact] - public async Task Page_When_PageNumber_Is_Zero_Pretends_PageNumber_Is_One() - { - var todoItems = DbSetMock.Create(TodoItems(2, 3, 1)).Object; - var repository = GetRepository(); - - var result = await repository.PageAsync(todoItems, 1, 0); - - Assert.Equal(TodoItems(2), result, new IdComparer()); - } - - [Fact] - public async Task Page_When_PageNumber_Of_PageSize_Does_Not_Exist_Return_Empty_Queryable() - { - var todoItems = DbSetMock.Create(TodoItems(2, 3, 1)).Object; - var repository = GetRepository(); - - var result = await repository.PageAsync(todoItems, 2, 3); - - Assert.Empty(result); - } - - [Theory] - [InlineData(3, 2, new[] { 4, 5, 6 })] - [InlineData(8, 2, new[] { 9 })] - [InlineData(20, 1, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 })] - public async Task Page_When_PageNumber_Is_Positive_Returns_PageNumberTh_Page_Of_Size_PageSize(int pageSize, int pageNumber, int[] expectedResult) - { - var todoItems = DbSetMock.Create(TodoItems(1, 2, 3, 4, 5, 6, 7, 8, 9)).Object; - var repository = GetRepository(); - - var result = await repository.PageAsync(todoItems, pageSize, pageNumber); - - Assert.Equal(TodoItems(expectedResult), result, new IdComparer()); - } - - [Theory] - [InlineData(6, -1, new[] { 4, 5, 6, 7, 8, 9 })] - [InlineData(6, -2, new[] { 1, 2, 3 })] - [InlineData(20, -1, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 })] - public async Task Page_When_PageNumber_Is_Negative_Returns_PageNumberTh_Page_From_End(int pageSize, int pageNumber, int[] expectedIds) - { - var todoItems = DbSetMock.Create(TodoItems(1, 2, 3, 4, 5, 6, 7, 8, 9)).Object; - var repository = GetRepository(); - - var result = await repository.PageAsync(todoItems, pageSize, pageNumber); - - Assert.Equal(TodoItems(expectedIds), result, new IdComparer()); - } - - private static TodoItem[] TodoItems(params int[] ids) - { - return ids.Select(id => new TodoItem { Id = id }).ToArray(); - } - - private class IdComparer : IEqualityComparer - where T : IIdentifiable - { - public bool Equals(T x, T y) => x?.StringId == y?.StringId; - - public int GetHashCode(T obj) => obj?.StringId?.GetHashCode() ?? 0; - } - } -} diff --git a/test/UnitTests/DbSetMock.cs b/test/UnitTests/DbSetMock.cs index f56a3b1f01..f6ad7a2915 100644 --- a/test/UnitTests/DbSetMock.cs +++ b/test/UnitTests/DbSetMock.cs @@ -25,14 +25,19 @@ public static Mock> AsDbSetMock(this List list) where T : class dbSetMock.As>().Setup(x => x.Expression).Returns(queryableList.Expression); dbSetMock.As>().Setup(x => x.ElementType).Returns(queryableList.ElementType); dbSetMock.As>().Setup(x => x.GetEnumerator()).Returns(queryableList.GetEnumerator()); - - dbSetMock.As>().Setup(m => m.GetEnumerator()).Returns(new TestAsyncEnumerator(queryableList.GetEnumerator())); + + var toReturn = new TestAsyncEnumerator(queryableList.GetEnumerator()); + + + dbSetMock.As>() + .Setup(m => m.GetAsyncEnumerator(It.IsAny())) + .Returns(toReturn); dbSetMock.As>().Setup(m => m.Provider).Returns(new TestAsyncQueryProvider(queryableList.Provider)); return dbSetMock; } } -internal class TestAsyncQueryProvider : IAsyncQueryProvider +internal class TestAsyncQueryProvider : IAsyncQueryProvider { private readonly IQueryProvider _inner; @@ -43,7 +48,7 @@ internal TestAsyncQueryProvider(IQueryProvider inner) public IQueryable CreateQuery(Expression expression) { - return new TestAsyncEnumerable(expression); + return new TestAsyncEnumerable(expression); } public IQueryable CreateQuery(Expression expression) @@ -70,6 +75,12 @@ public Task ExecuteAsync(Expression expression, CancellationTo { return Task.FromResult(Execute(expression)); } + + TResult IAsyncQueryProvider.ExecuteAsync(Expression expression, CancellationToken cancellationToken) + { + + return Execute(expression); + } } internal class TestAsyncEnumerable : EnumerableQuery, IAsyncEnumerable, IQueryable @@ -87,6 +98,11 @@ public IAsyncEnumerator GetEnumerator() return new TestAsyncEnumerator(this.AsEnumerable().GetEnumerator()); } + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException(); + } + IQueryProvider IQueryable.Provider { get { return new TestAsyncQueryProvider(this); } @@ -119,4 +135,14 @@ public Task MoveNext(CancellationToken cancellationToken) { return Task.FromResult(_inner.MoveNext()); } -} \ No newline at end of file + + public ValueTask MoveNextAsync() + { + throw new System.NotImplementedException(); + } + + public ValueTask DisposeAsync() + { + throw new System.NotImplementedException(); + } +} diff --git a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index 3b49dda9bf..d23b1f4d9d 100644 --- a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -1,11 +1,9 @@ -using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Data; using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Formatters; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Generics; -using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -17,6 +15,10 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Serialization.Server.Builders; +using JsonApiDotNetCore.Serialization.Server; namespace UnitTests.Extensions { @@ -25,47 +27,48 @@ public class IServiceCollectionExtensionsTests [Fact] public void AddJsonApiInternals_Adds_All_Required_Services() { - // arrange + // Arrange var services = new ServiceCollection(); - var jsonApiOptions = new JsonApiOptions(); services.AddDbContext(options => options.UseInMemoryDatabase("UnitTestDb"), ServiceLifetime.Transient); + services.AddJsonApi(); - // act - services.AddJsonApiInternals(jsonApiOptions); + // Act // this is required because the DbContextResolver requires access to the current HttpContext // to get the request scoped DbContext instance services.AddScoped(); var provider = services.BuildServiceProvider(); - // assert + // Assert + var currentRequest = provider.GetService(); + Assert.NotNull(currentRequest); + var resourceGraph = provider.GetService(); + Assert.NotNull(resourceGraph); + currentRequest.SetRequestResource(resourceGraph.GetResourceContext()); + Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService()); - Assert.NotNull(provider.GetService(typeof(IEntityRepository))); - Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService(typeof(IResourceRepository))); Assert.NotNull(provider.GetService()); - Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService()); - Assert.NotNull(provider.GetService()); - Assert.NotNull(provider.GetService()); - Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService>()); + Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService()); - Assert.NotNull(provider.GetService()); - Assert.NotNull(provider.GetService()); - Assert.NotNull(provider.GetService()); - Assert.NotNull(provider.GetService(typeof(GenericProcessor))); + Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService(typeof(RepositoryRelationshipUpdateHelper))); } [Fact] public void AddResourceService_Registers_All_Shorthand_Service_Interfaces() { - // arrange + // Arrange var services = new ServiceCollection(); - // act + // Act services.AddResourceService(); - - // assert + + // Assert var provider = services.BuildServiceProvider(); Assert.IsType(provider.GetService(typeof(IResourceService))); Assert.IsType(provider.GetService(typeof(IResourceCmdService))); @@ -82,13 +85,13 @@ public void AddResourceService_Registers_All_Shorthand_Service_Interfaces() [Fact] public void AddResourceService_Registers_All_LongForm_Service_Interfaces() { - // arrange + // Arrange var services = new ServiceCollection(); - // act + // Act services.AddResourceService(); - - // assert + + // Assert var provider = services.BuildServiceProvider(); Assert.IsType(provider.GetService(typeof(IResourceService))); Assert.IsType(provider.GetService(typeof(IResourceCmdService))); @@ -105,29 +108,29 @@ public void AddResourceService_Registers_All_LongForm_Service_Interfaces() [Fact] public void AddResourceService_Throws_If_Type_Does_Not_Implement_Any_Interfaces() { - // arrange + // Arrange var services = new ServiceCollection(); - - // act, assert + + // Act, assert Assert.Throws(() => services.AddResourceService()); } [Fact] public void AddJsonApi_With_Context_Uses_DbSet_PropertyName_If_NoOtherSpecified() { - // arrange + // Arrange var services = new ServiceCollection(); services.AddScoped(); - // act + // Act services.AddJsonApi(); - // assert + // Assert var provider = services.BuildServiceProvider(); - var graph = provider.GetService(); - var resource = graph.GetContextEntity(typeof(IntResource)); - Assert.Equal("resource", resource.EntityName); + var resourceGraph = provider.GetService(); + var resource = resourceGraph.GetResourceContext(typeof(IntResource)); + Assert.Equal("resource", resource.ResourceName); } public class IntResource : Identifiable { } @@ -140,9 +143,9 @@ private class IntResourceService : IResourceService public Task> GetAsync() => throw new NotImplementedException(); public Task GetAsync(int id) => throw new NotImplementedException(); public Task GetRelationshipAsync(int id, string relationshipName) => throw new NotImplementedException(); - public Task GetRelationshipsAsync(int id, string relationshipName) => throw new NotImplementedException(); + public Task GetRelationshipsAsync(int id, string relationshipName) => throw new NotImplementedException(); public Task UpdateAsync(int id, IntResource entity) => throw new NotImplementedException(); - public Task UpdateRelationshipsAsync(int id, string relationshipName, List relationships) => throw new NotImplementedException(); + public Task UpdateRelationshipsAsync(int id, string relationshipName, object relationships) => throw new NotImplementedException(); } private class GuidResourceService : IResourceService @@ -152,13 +155,13 @@ private class GuidResourceService : IResourceService public Task> GetAsync() => throw new NotImplementedException(); public Task GetAsync(Guid id) => throw new NotImplementedException(); public Task GetRelationshipAsync(Guid id, string relationshipName) => throw new NotImplementedException(); - public Task GetRelationshipsAsync(Guid id, string relationshipName) => throw new NotImplementedException(); + public Task GetRelationshipsAsync(Guid id, string relationshipName) => throw new NotImplementedException(); public Task UpdateAsync(Guid id, GuidResource entity) => throw new NotImplementedException(); - public Task UpdateRelationshipsAsync(Guid id, string relationshipName, List relationships) => throw new NotImplementedException(); + public Task UpdateRelationshipsAsync(Guid id, string relationshipName, object relationships) => throw new NotImplementedException(); } - public class TestContext : DbContext + public class TestContext : DbContext { public DbSet Resource { get; set; } } diff --git a/test/UnitTests/Extensions/TypeExtensions_Tests.cs b/test/UnitTests/Extensions/TypeExtensions_Tests.cs index f565019f26..b473ceb5e3 100644 --- a/test/UnitTests/Extensions/TypeExtensions_Tests.cs +++ b/test/UnitTests/Extensions/TypeExtensions_Tests.cs @@ -11,13 +11,13 @@ public class TypeExtensions_Tests [Fact] public void GetCollection_Creates_List_If_T_Implements_Interface() { - // arrange + // Arrange var type = typeof(Model); - // act + // Act var collection = type.GetEmptyCollection(); - // assert + // Assert Assert.NotNull(collection); Assert.Empty(collection); Assert.IsType>(collection); @@ -26,13 +26,13 @@ public void GetCollection_Creates_List_If_T_Implements_Interface() [Fact] public void New_Creates_An_Instance_If_T_Implements_Interface() { - // arrange + // Arrange var type = typeof(Model); - // act + // Act var instance = type.New(); - // assert + // Assert Assert.NotNull(instance); Assert.IsType(instance); } @@ -40,26 +40,26 @@ public void New_Creates_An_Instance_If_T_Implements_Interface() [Fact] public void Implements_Returns_True_If_Type_Implements_Interface() { - // arrange + // Arrange var type = typeof(Model); - // act + // Act var result = type.Implements(); - // assert + // Assert Assert.True(result); } [Fact] public void Implements_Returns_False_If_Type_DoesNot_Implement_Interface() { - // arrange + // Arrange var type = typeof(String); - // act + // Act var result = type.Implements(); - // assert + // Assert Assert.False(result); } diff --git a/test/UnitTests/Graph/TypeLocator_Tests.cs b/test/UnitTests/Graph/TypeLocator_Tests.cs index 890994c340..26381ea1fb 100644 --- a/test/UnitTests/Graph/TypeLocator_Tests.cs +++ b/test/UnitTests/Graph/TypeLocator_Tests.cs @@ -11,7 +11,7 @@ public class TypeLocator_Tests [Fact] public void GetGenericInterfaceImplementation_Gets_Implementation() { - // arrange + // Arrange var assembly = GetType().Assembly; var openGeneric = typeof(IGenericInterface<>); var genericArg = typeof(int); @@ -19,37 +19,36 @@ public void GetGenericInterfaceImplementation_Gets_Implementation() var expectedImplementation = typeof(Implementation); var expectedInterface = typeof(IGenericInterface); - // act - var result = TypeLocator.GetGenericInterfaceImplementation( + // Act + var (implementation, registrationInterface) = TypeLocator.GetGenericInterfaceImplementation( assembly, openGeneric, genericArg ); - // assert - Assert.NotNull(result); - Assert.Equal(expectedImplementation, result.implementation); - Assert.Equal(expectedInterface, result.registrationInterface); + // Assert + Assert.Equal(expectedImplementation, implementation); + Assert.Equal(expectedInterface, registrationInterface); } [Fact] public void GetDerivedGenericTypes_Gets_Implementation() { - // arrange + // Arrange var assembly = GetType().Assembly; var openGeneric = typeof(BaseType<>); var genericArg = typeof(int); var expectedImplementation = typeof(DerivedType); - // act + // Act var results = TypeLocator.GetDerivedGenericTypes( assembly, openGeneric, genericArg ); - // assert + // Assert Assert.NotNull(results); var result = Assert.Single(results); Assert.Equal(expectedImplementation, result); @@ -58,58 +57,56 @@ public void GetDerivedGenericTypes_Gets_Implementation() [Fact] public void GetIdType_Correctly_Identifies_JsonApiResource() { - // arrange + // Arrange var type = typeof(Model); var exextedIdType = typeof(int); - // act - var result = TypeLocator.GetIdType(type); + // Act + var (isJsonApiResource, idType) = TypeLocator.GetIdType(type); - // assert - Assert.NotNull(result); - Assert.True(result.isJsonApiResource); - Assert.Equal(exextedIdType, result.idType); + // Assert + Assert.True(isJsonApiResource); + Assert.Equal(exextedIdType, idType); } [Fact] public void GetIdType_Correctly_Identifies_NonJsonApiResource() { - // arrange + // Arrange var type = typeof(DerivedType); Type exextedIdType = null; - // act - var result = TypeLocator.GetIdType(type); + // Act + var (isJsonApiResource, idType) = TypeLocator.GetIdType(type); - // assert - Assert.NotNull(result); - Assert.False(result.isJsonApiResource); - Assert.Equal(exextedIdType, result.idType); + // Assert + Assert.False(isJsonApiResource); + Assert.Equal(exextedIdType, idType); } [Fact] public void GetIdentifableTypes_Locates_Identifiable_Resource() { - // arrange + // Arrange var resourceType = typeof(Model); - // act + // Act var results = TypeLocator.GetIdentifableTypes(resourceType.Assembly); - // assert + // Assert Assert.Contains(results, r => r.ResourceType == resourceType); } [Fact] public void GetIdentifableTypes__Only_Contains_IIdentifiable_Types() { - // arrange + // Arrange var resourceType = typeof(Model); - // act + // Act var resourceDescriptors = TypeLocator.GetIdentifableTypes(resourceType.Assembly); - // assert + // Assert foreach(var resourceDescriptor in resourceDescriptors) Assert.True(typeof(IIdentifiable).IsAssignableFrom(resourceDescriptor.ResourceType)); } @@ -117,13 +114,13 @@ public void GetIdentifableTypes__Only_Contains_IIdentifiable_Types() [Fact] public void TryGetResourceDescriptor_Returns_True_If_Type_Is_IIdentfiable() { - // arrange + // Arrange var resourceType = typeof(Model); - // act + // Act var isJsonApiResource = TypeLocator.TryGetResourceDescriptor(resourceType, out var descriptor); - // assert + // Assert Assert.True(isJsonApiResource); Assert.Equal(resourceType, descriptor.ResourceType); Assert.Equal(typeof(int), descriptor.IdType); @@ -132,13 +129,13 @@ public void TryGetResourceDescriptor_Returns_True_If_Type_Is_IIdentfiable() [Fact] public void TryGetResourceDescriptor_Returns_False_If_Type_Is_IIdentfiable() { - // arrange + // Arrange var resourceType = typeof(String); - // act - var isJsonApiResource = TypeLocator.TryGetResourceDescriptor(resourceType, out var descriptor); + // Act + var isJsonApiResource = TypeLocator.TryGetResourceDescriptor(resourceType, out var _); - // assert + // Assert Assert.False(isJsonApiResource); } } @@ -152,4 +149,4 @@ public class BaseType { } public class DerivedType : BaseType { } public class Model : Identifiable { } -} \ No newline at end of file +} diff --git a/test/UnitTests/Internal/ContextGraphBuilder_Tests.cs b/test/UnitTests/Internal/ContextGraphBuilder_Tests.cs index 122b2cd67b..cf61c512d7 100644 --- a/test/UnitTests/Internal/ContextGraphBuilder_Tests.cs +++ b/test/UnitTests/Internal/ContextGraphBuilder_Tests.cs @@ -11,28 +11,28 @@ public class ResourceGraphBuilder_Tests [Fact] public void AddDbContext_Does_Not_Throw_If_Context_Contains_Members_That_DoNot_Implement_IIdentifiable() { - // arrange + // Arrange var resourceGraphBuilder = new ResourceGraphBuilder(); - // act + // Act resourceGraphBuilder.AddDbContext(); var resourceGraph = resourceGraphBuilder.Build() as ResourceGraph; - // assert - Assert.Empty(resourceGraph.Entities); + // Assert + Assert.Empty(resourceGraph.GetResourceContexts()); } [Fact] public void Adding_DbContext_Members_That_DoNot_Implement_IIdentifiable_Creates_Warning() { - // arrange + // Arrange var resourceGraphBuilder = new ResourceGraphBuilder(); - // act + // Act resourceGraphBuilder.AddDbContext(); var resourceGraph = resourceGraphBuilder.Build() as ResourceGraph; - // assert + // Assert Assert.Single(resourceGraph.ValidationResults); Assert.Contains(resourceGraph.ValidationResults, v => v.LogLevel == LogLevel.Warning); } diff --git a/test/UnitTests/Internal/TypeHelper_Tests.cs b/test/UnitTests/Internal/TypeHelper_Tests.cs index 1c105659f5..287444aea4 100644 --- a/test/UnitTests/Internal/TypeHelper_Tests.cs +++ b/test/UnitTests/Internal/TypeHelper_Tests.cs @@ -10,45 +10,45 @@ public class TypeHelper_Tests [Fact] public void Can_Convert_DateTimeOffsets() { - // arrange + // Arrange var dto = DateTimeOffset.Now; var formattedString = dto.ToString("O"); - // act + // Act var result = TypeHelper.ConvertType(formattedString, typeof(DateTimeOffset)); - // assert + // Assert Assert.Equal(dto, result); } [Fact] public void Bad_DateTimeOffset_String_Throws() { - // arrange + // Arrange var formattedString = "this_is_not_a_valid_dto"; - // act - // assert + // Act + // Assert Assert.Throws(() => TypeHelper.ConvertType(formattedString, typeof(DateTimeOffset))); } [Fact] public void Can_Convert_Enums() { - // arrange + // Arrange var formattedString = "1"; - // act + // Act var result = TypeHelper.ConvertType(formattedString, typeof(TestEnum)); - // assert + // Assert Assert.Equal(TestEnum.Test, result); } [Fact] public void ConvertType_Returns_Value_If_Type_Is_Same() { - // arrange + // Arrange var val = new ComplexType { Property = 1 @@ -56,17 +56,17 @@ public void ConvertType_Returns_Value_If_Type_Is_Same() var type = val.GetType(); - // act + // Act var result = TypeHelper.ConvertType(val, type); - // assert + // Assert Assert.Equal(val, result); } [Fact] public void ConvertType_Returns_Value_If_Type_Is_Assignable() { - // arrange + // Arrange var val = new ComplexType { Property = 1 @@ -75,11 +75,11 @@ public void ConvertType_Returns_Value_If_Type_Is_Assignable() var baseType = typeof(BaseType); var iType = typeof(IType); - // act + // Act var baseResult = TypeHelper.ConvertType(val, baseType); var iResult = TypeHelper.ConvertType(val, iType); - // assert + // Assert Assert.Equal(val, baseResult); Assert.Equal(val, iResult); } @@ -87,7 +87,7 @@ public void ConvertType_Returns_Value_If_Type_Is_Assignable() [Fact] public void ConvertType_Returns_Default_Value_For_Empty_Strings() { - // arrange -- can't use non-constants in [Theory] + // Arrange -- can't use non-constants in [Theory] var data = new Dictionary { { typeof(int), 0 }, @@ -99,10 +99,10 @@ public void ConvertType_Returns_Default_Value_For_Empty_Strings() foreach (var t in data) { - // act + // Act var result = TypeHelper.ConvertType(string.Empty, t.Key); - // assert + // Assert Assert.Equal(t.Value, result); } } @@ -124,10 +124,10 @@ public void Can_Convert_TimeSpans() [Fact] public void Bad_TimeSpanString_Throws() { - // arrange + // Arrange var formattedString = "this_is_not_a_valid_timespan"; - // act/assert + // Act/assert Assert.Throws(() => TypeHelper.ConvertType(formattedString, typeof(TimeSpan))); } diff --git a/test/UnitTests/Models/LinkTests.cs b/test/UnitTests/Models/LinkTests.cs index e954ddf135..f8f163fa03 100644 --- a/test/UnitTests/Models/LinkTests.cs +++ b/test/UnitTests/Models/LinkTests.cs @@ -1,4 +1,5 @@ using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.Links; using Xunit; namespace UnitTests.Models @@ -8,10 +9,10 @@ public class LinkTests [Fact] public void All_Contains_All_Flags_Except_None() { - // arrange + // Arrange var e = Link.All; - // assert + // Assert Assert.True(e.HasFlag(Link.Self)); Assert.True(e.HasFlag(Link.Paging)); Assert.True(e.HasFlag(Link.Related)); @@ -22,10 +23,10 @@ public void All_Contains_All_Flags_Except_None() [Fact] public void None_Contains_Only_None() { - // arrange + // Arrange var e = Link.None; - // assert + // Assert Assert.False(e.HasFlag(Link.Self)); Assert.False(e.HasFlag(Link.Paging)); Assert.False(e.HasFlag(Link.Related)); @@ -36,10 +37,10 @@ public void None_Contains_Only_None() [Fact] public void Self() { - // arrange + // Arrange var e = Link.Self; - // assert + // Assert Assert.True(e.HasFlag(Link.Self)); Assert.False(e.HasFlag(Link.Paging)); Assert.False(e.HasFlag(Link.Related)); @@ -50,10 +51,10 @@ public void Self() [Fact] public void Paging() { - // arrange + // Arrange var e = Link.Paging; - // assert + // Assert Assert.False(e.HasFlag(Link.Self)); Assert.True(e.HasFlag(Link.Paging)); Assert.False(e.HasFlag(Link.Related)); @@ -64,10 +65,10 @@ public void Paging() [Fact] public void Related() { - // arrange + // Arrange var e = Link.Related; - // assert + // Assert Assert.False(e.HasFlag(Link.Self)); Assert.False(e.HasFlag(Link.Paging)); Assert.True(e.HasFlag(Link.Related)); diff --git a/test/UnitTests/Models/RelationshipDataTests.cs b/test/UnitTests/Models/RelationshipDataTests.cs index ff00144b62..eaa52440e6 100644 --- a/test/UnitTests/Models/RelationshipDataTests.cs +++ b/test/UnitTests/Models/RelationshipDataTests.cs @@ -8,10 +8,10 @@ namespace UnitTests.Models public class RelationshipDataTests { [Fact] - public void Setting_ExposedData_To_List_Sets_ManyData() + public void Setting_ExposeData_To_List_Sets_ManyData() { - // arrange - var relationshipData = new RelationshipData(); + // Arrange + var relationshipData = new RelationshipEntry(); var relationships = new List { new ResourceIdentifierObject { Id = "9", @@ -19,21 +19,21 @@ public void Setting_ExposedData_To_List_Sets_ManyData() } }; - // act - relationshipData.ExposedData = relationships; + // Act + relationshipData.Data = relationships; - // assert + // Assert Assert.NotEmpty(relationshipData.ManyData); Assert.Equal("authors", relationshipData.ManyData[0].Type); Assert.Equal("9", relationshipData.ManyData[0].Id); - Assert.True(relationshipData.IsHasMany); + Assert.True(relationshipData.IsManyData); } [Fact] - public void Setting_ExposedData_To_JArray_Sets_ManyData() + public void Setting_ExposeData_To_JArray_Sets_ManyData() { - // arrange - var relationshipData = new RelationshipData(); + // Arrange + var relationshipData = new RelationshipEntry(); var relationshipsJson = @"[ { ""type"": ""authors"", @@ -43,41 +43,41 @@ public void Setting_ExposedData_To_JArray_Sets_ManyData() var relationships = JArray.Parse(relationshipsJson); - // act - relationshipData.ExposedData = relationships; + // Act + relationshipData.Data = relationships; - // assert + // Assert Assert.NotEmpty(relationshipData.ManyData); Assert.Equal("authors", relationshipData.ManyData[0].Type); Assert.Equal("9", relationshipData.ManyData[0].Id); - Assert.True(relationshipData.IsHasMany); + Assert.True(relationshipData.IsManyData); } [Fact] - public void Setting_ExposedData_To_RIO_Sets_SingleData() + public void Setting_ExposeData_To_RIO_Sets_SingleData() { - // arrange - var relationshipData = new RelationshipData(); + // Arrange + var relationshipData = new RelationshipEntry(); var relationship = new ResourceIdentifierObject { Id = "9", Type = "authors" }; - // act - relationshipData.ExposedData = relationship; + // Act + relationshipData.Data = relationship; - // assert + // Assert Assert.NotNull(relationshipData.SingleData); Assert.Equal("authors", relationshipData.SingleData.Type); Assert.Equal("9", relationshipData.SingleData.Id); - Assert.False(relationshipData.IsHasMany); + Assert.False(relationshipData.IsManyData); } [Fact] - public void Setting_ExposedData_To_JObject_Sets_SingleData() + public void Setting_ExposeData_To_JObject_Sets_SingleData() { - // arrange - var relationshipData = new RelationshipData(); + // Arrange + var relationshipData = new RelationshipEntry(); var relationshipJson = @"{ ""id"": ""9"", ""type"": ""authors"" @@ -85,14 +85,14 @@ public void Setting_ExposedData_To_JObject_Sets_SingleData() var relationship = JObject.Parse(relationshipJson); - // act - relationshipData.ExposedData = relationship; + // Act + relationshipData.Data = relationship; - // assert + // Assert Assert.NotNull(relationshipData.SingleData); Assert.Equal("authors", relationshipData.SingleData.Type); Assert.Equal("9", relationshipData.SingleData.Id); - Assert.False(relationshipData.IsHasMany); + Assert.False(relationshipData.IsManyData); } } } diff --git a/test/UnitTests/Models/ResourceDefinitionTests.cs b/test/UnitTests/Models/ResourceDefinitionTests.cs index 5abba48ca8..41529fc742 100644 --- a/test/UnitTests/Models/ResourceDefinitionTests.cs +++ b/test/UnitTests/Models/ResourceDefinitionTests.cs @@ -1,8 +1,8 @@ using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Models; -using System.Collections.Generic; +using JsonApiDotNetCore.Services; using System.Linq; using Xunit; @@ -10,11 +10,11 @@ namespace UnitTests.Models { public class ResourceDefinition_Scenario_Tests { - private readonly IResourceGraph _graph; + private readonly IResourceGraph _resourceGraph; public ResourceDefinition_Scenario_Tests() { - _graph = new ResourceGraphBuilder() + _resourceGraph = new ResourceGraphBuilder() .AddResource("models") .Build(); } @@ -22,78 +22,29 @@ public ResourceDefinition_Scenario_Tests() [Fact] public void Request_Filter_Uses_Member_Expression() { - // arrange + // Arrange var resource = new RequestFilteredResource(isAdmin: true); - // act - var attrs = resource.GetOutputAttrs(null); + // Act + var attrs = resource.GetAllowedAttributes(); - // assert + // Assert Assert.DoesNotContain(attrs, a => a.InternalAttributeName == nameof(Model.AlwaysExcluded)); } [Fact] public void Request_Filter_Uses_NewExpression() { - // arrange + // Arrange var resource = new RequestFilteredResource(isAdmin: false); - // act - var attrs = resource.GetOutputAttrs(null); + // Act + var attrs = resource.GetAllowedAttributes(); - // assert + // Assert Assert.DoesNotContain(attrs, a => a.InternalAttributeName == nameof(Model.AlwaysExcluded)); Assert.DoesNotContain(attrs, a => a.InternalAttributeName == nameof(Model.Password)); } - - [Fact] - public void Instance_Filter_Uses_Member_Expression() - { - // arrange - var model = new Model { AlwaysExcluded = "Admin" }; - var resource = new InstanceFilteredResource(); - - // act - var attrs = resource.GetOutputAttrs(model); - - // assert - Assert.DoesNotContain(attrs, a => a.InternalAttributeName == nameof(Model.AlwaysExcluded)); - } - - [Fact] - public void Instance_Filter_Uses_NewExpression() - { - // arrange - var model = new Model { AlwaysExcluded = "Joe" }; - var resource = new InstanceFilteredResource(); - - // act - var attrs = resource.GetOutputAttrs(model); - - // assert - Assert.DoesNotContain(attrs, a => a.InternalAttributeName == nameof(Model.AlwaysExcluded)); - Assert.DoesNotContain(attrs, a => a.InternalAttributeName == nameof(Model.Password)); - } - - [Fact] - public void InstanceOutputAttrsAreSpecified_Returns_True_If_Instance_Method_Is_Overriden() - { - // act - var resource = new InstanceFilteredResource(); - - // assert - Assert.True(resource._instanceAttrsAreSpecified); - } - - [Fact] - public void InstanceOutputAttrsAreSpecified_Returns_False_If_Instance_Method_Is_Not_Overriden() - { - // act - var resource = new RequestFilteredResource(isAdmin: false); - - // assert - Assert.False(resource._instanceAttrsAreSpecified); - } } public class Model : Identifiable @@ -105,21 +56,16 @@ public class Model : Identifiable public class RequestFilteredResource : ResourceDefinition { - private readonly bool _isAdmin; - // this constructor will be resolved from the container // that means you can take on any dependency that is also defined in the container - public RequestFilteredResource(bool isAdmin) : base (new ResourceGraphBuilder().AddResource().Build()) + public RequestFilteredResource(bool isAdmin) : base(new ResourceGraphBuilder().AddResource().Build()) { - _isAdmin = isAdmin; + if (isAdmin) + HideFields(m => m.AlwaysExcluded); + else + HideFields(m => new { m.AlwaysExcluded, m.Password }); } - // Called once per filtered resource in request. - protected override List OutputAttrs() - => _isAdmin - ? Remove(m => m.AlwaysExcluded) - : Remove(m => new { m.AlwaysExcluded, m.Password }, from: base.OutputAttrs()); - public override QueryFilters GetQueryFilters() => new QueryFilters { { "is-active", (query, value) => query.Select(x => x) } @@ -129,17 +75,4 @@ public override PropertySortOrder GetDefaultSortOrder() (t => t.Prop, SortDirection.Ascending) }; } - - public class InstanceFilteredResource : ResourceDefinition - { - public InstanceFilteredResource() : base(new ResourceGraphBuilder().AddResource().Build()) - { - } - - // Called once per resource instance - protected override List OutputAttrs(Model model) - => model.AlwaysExcluded == "Admin" - ? Remove(m => m.AlwaysExcluded, base.OutputAttrs()) - : Remove(m => new { m.AlwaysExcluded, m.Password }, from: base.OutputAttrs()); - } -} \ No newline at end of file +} diff --git a/test/UnitTests/QueryParameters/FilterServiceTests.cs b/test/UnitTests/QueryParameters/FilterServiceTests.cs new file mode 100644 index 0000000000..68937ee793 --- /dev/null +++ b/test/UnitTests/QueryParameters/FilterServiceTests.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Query; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace UnitTests.QueryParameters +{ + public class FilterServiceTests : QueryParametersUnitTestCollection + { + public FilterService GetService() + { + return new FilterService(MockResourceDefinitionProvider(), _resourceGraph, MockCurrentRequest(_articleResourceContext)); + } + + [Fact] + public void Name_FilterService_IsCorrect() + { + // Arrange + var filterService = GetService(); + + // Act + var name = filterService.Name; + + // Assert + Assert.Equal("filter", name); + } + + [Theory] + [InlineData("title", "", "value")] + [InlineData("title", "eq:", "value")] + [InlineData("title", "lt:", "value")] + [InlineData("title", "gt:", "value")] + [InlineData("title", "le:", "value")] + [InlineData("title", "ge:", "value")] + [InlineData("title", "like:", "value")] + [InlineData("title", "ne:", "value")] + [InlineData("title", "in:", "value")] + [InlineData("title", "nin:", "value")] + [InlineData("title", "isnull:", "")] + [InlineData("title", "isnotnull:", "")] + [InlineData("title", "", "2017-08-15T22:43:47.0156350-05:00")] + [InlineData("title", "le:", "2017-08-15T22:43:47.0156350-05:00")] + public void Parse_ValidFilters_CanParse(string key, string @operator, string value) + { + // Arrange + var queryValue = @operator + value; + var query = new KeyValuePair($"filter[{key}]", new StringValues(queryValue)); + var filterService = GetService(); + + // Act + filterService.Parse(query); + var filter = filterService.Get().Single(); + + // Assert + if (!string.IsNullOrEmpty(@operator)) + Assert.Equal(@operator.Replace(":", ""), filter.Operation.ToString("G")); + else + Assert.Equal(FilterOperation.eq, filter.Operation); + + if (!string.IsNullOrEmpty(value)) + Assert.Equal(value, filter.Value); + } + } +} diff --git a/test/UnitTests/QueryParameters/IncludeServiceTests.cs b/test/UnitTests/QueryParameters/IncludeServiceTests.cs new file mode 100644 index 0000000000..2230ed0b41 --- /dev/null +++ b/test/UnitTests/QueryParameters/IncludeServiceTests.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Query; +using Microsoft.Extensions.Primitives; +using UnitTests.TestModels; +using Xunit; + +namespace UnitTests.QueryParameters +{ + public class IncludeServiceTests : QueryParametersUnitTestCollection + { + + public IncludeService GetService(ResourceContext resourceContext = null) + { + return new IncludeService(_resourceGraph, MockCurrentRequest(resourceContext ?? _articleResourceContext)); + } + + [Fact] + public void Name_IncludeService_IsCorrect() + { + // Arrange + var filterService = GetService(); + + // Act + var name = filterService.Name; + + // Assert + Assert.Equal("include", name); + } + + [Fact] + public void Parse_MultipleNestedChains_CanParse() + { + // Arrange + const string chain = "author.blogs.reviewer.favorite-food,reviewer.blogs.author.favorite-song"; + var query = new KeyValuePair("include", new StringValues(chain)); + var service = GetService(); + + // Act + service.Parse(query); + + // Assert + var chains = service.Get(); + Assert.Equal(2, chains.Count); + var firstChain = chains[0]; + Assert.Equal("author", firstChain.First().PublicRelationshipName); + Assert.Equal("favorite-food", firstChain.Last().PublicRelationshipName); + var secondChain = chains[1]; + Assert.Equal("reviewer", secondChain.First().PublicRelationshipName); + Assert.Equal("favorite-song", secondChain.Last().PublicRelationshipName); + } + + [Fact] + public void Parse_ChainsOnWrongMainResource_ThrowsJsonApiException() + { + // Arrange + const string chain = "author.blogs.reviewer.favorite-food,reviewer.blogs.author.favorite-song"; + var query = new KeyValuePair("include", new StringValues(chain)); + var service = GetService(_resourceGraph.GetResourceContext()); + + // Act, assert + var exception = Assert.Throws( () => service.Parse(query)); + Assert.Contains("Invalid", exception.Message); + } + + [Fact] + public void Parse_NotIncludable_ThrowsJsonApiException() + { + // Arrange + const string chain = "cannot-include"; + var query = new KeyValuePair("include", new StringValues(chain)); + var service = GetService(); + + // Act, assert + var exception = Assert.Throws(() => service.Parse(query)); + Assert.Contains("not allowed", exception.Message); + } + + [Fact] + public void Parse_NonExistingRelationship_ThrowsJsonApiException() + { + // Arrange + const string chain = "nonsense"; + var query = new KeyValuePair("include", new StringValues(chain)); + var service = GetService(); + + // Act, assert + var exception = Assert.Throws(() => service.Parse(query)); + Assert.Contains("Invalid", exception.Message); + } + + [Fact] + public void Parse_EmptyChain_ThrowsJsonApiException() + { + // Arrange + const string chain = ""; + var query = new KeyValuePair("include", new StringValues(chain)); + var service = GetService(); + + // Act, assert + var exception = Assert.Throws(() => service.Parse(query)); + Assert.Contains("Include parameter must not be empty if provided", exception.Message); + } + } +} diff --git a/test/UnitTests/QueryParameters/OmitDefaultService.cs b/test/UnitTests/QueryParameters/OmitDefaultService.cs new file mode 100644 index 0000000000..522c15049c --- /dev/null +++ b/test/UnitTests/QueryParameters/OmitDefaultService.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Query; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace UnitTests.QueryParameters +{ + public class OmitDefaultServiceTests : QueryParametersUnitTestCollection + { + public OmitDefaultService GetService(bool @default, bool @override) + { + var options = new JsonApiOptions + { + DefaultAttributeResponseBehavior = new DefaultAttributeResponseBehavior(@default, @override) + }; + + return new OmitDefaultService(options); + } + + [Fact] + public void Name_OmitNullService_IsCorrect() + { + // Arrange + var service = GetService(true, true); + + // Act + var name = service.Name; + + // Assert + Assert.Equal("omitdefault", name); + } + + [Theory] + [InlineData("false", true, true, false)] + [InlineData("false", true, false, true)] + [InlineData("true", false, true, true)] + [InlineData("true", false, false, false)] + public void Parse_QueryConfigWithApiSettings_CanParse(string queryConfig, bool @default, bool @override, bool expected) + { + // Arrange + var query = new KeyValuePair($"omitNull", new StringValues(queryConfig)); + var service = GetService(@default, @override); + + // Act + service.Parse(query); + + // Assert + Assert.Equal(expected, service.Config); + } + } +} diff --git a/test/UnitTests/QueryParameters/OmitNullService.cs b/test/UnitTests/QueryParameters/OmitNullService.cs new file mode 100644 index 0000000000..f4ec0e0ed5 --- /dev/null +++ b/test/UnitTests/QueryParameters/OmitNullService.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Query; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace UnitTests.QueryParameters +{ + public class OmitNullServiceTests : QueryParametersUnitTestCollection + { + public OmitNullService GetService(bool @default, bool @override) + { + var options = new JsonApiOptions + { + NullAttributeResponseBehavior = new NullAttributeResponseBehavior(@default, @override) + }; + + return new OmitNullService(options); + } + + [Fact] + public void Name_OmitNullService_IsCorrect() + { + // Arrange + var service = GetService(true, true); + + // Act + var name = service.Name; + + // Assert + Assert.Equal("omitnull", name); + } + + [Theory] + [InlineData("false", true, true, false)] + [InlineData("false", true, false, true)] + [InlineData("true", false, true, true)] + [InlineData("true", false, false, false)] + public void Parse_QueryConfigWithApiSettings_CanParse(string queryConfig, bool @default, bool @override, bool expected) + { + // Arrange + var query = new KeyValuePair($"omitNull", new StringValues(queryConfig)); + var service = GetService(@default, @override); + + // Act + service.Parse(query); + + // Assert + Assert.Equal(expected, service.Config); + } + } +} diff --git a/test/UnitTests/QueryParameters/PageServiceTests.cs b/test/UnitTests/QueryParameters/PageServiceTests.cs new file mode 100644 index 0000000000..22373d7d86 --- /dev/null +++ b/test/UnitTests/QueryParameters/PageServiceTests.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Query; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace UnitTests.QueryParameters +{ + public class PageServiceTests : QueryParametersUnitTestCollection + { + public PageService GetService() + { + return new PageService(new JsonApiOptions()); + } + + [Fact] + public void Name_PageService_IsCorrect() + { + // Arrange + var filterService = GetService(); + + // Act + var name = filterService.Name; + + // Assert + Assert.Equal("page", name); + } + + [Theory] + [InlineData("1", 1, false)] + [InlineData("abcde", 0, true)] + [InlineData("", 0, true)] + public void Parse_PageSize_CanParse(string value, int expectedValue, bool shouldThrow) + { + // Arrange + var query = new KeyValuePair($"page[size]", new StringValues(value)); + var service = GetService(); + + // Act + if (shouldThrow) + { + var ex = Assert.Throws(() => service.Parse(query)); + Assert.Equal(400, ex.GetStatusCode()); + } + else + { + service.Parse(query); + Assert.Equal(expectedValue, service.PageSize); + } + } + + [Theory] + [InlineData("1", 1, false)] + [InlineData("abcde", 0, true)] + [InlineData("", 0, true)] + public void Parse_PageNumber_CanParse(string value, int expectedValue, bool shouldThrow) + { + // Arrange + var query = new KeyValuePair($"page[number]", new StringValues(value)); + var service = GetService(); + + + // Act + if (shouldThrow) + { + var ex = Assert.Throws(() => service.Parse(query)); + Assert.Equal(400, ex.GetStatusCode()); + } + else + { + service.Parse(query); + Assert.Equal(expectedValue, service.CurrentPage); + } + } + } +} diff --git a/test/UnitTests/QueryParameters/QueryParametersUnitTestCollection.cs b/test/UnitTests/QueryParameters/QueryParametersUnitTestCollection.cs new file mode 100644 index 0000000000..24136f87d8 --- /dev/null +++ b/test/UnitTests/QueryParameters/QueryParametersUnitTestCollection.cs @@ -0,0 +1,50 @@ +using System; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Query; +using Moq; +using UnitTests.TestModels; + +namespace UnitTests.QueryParameters +{ + public class QueryParametersUnitTestCollection + { + protected readonly ResourceContext _articleResourceContext; + protected readonly IResourceGraph _resourceGraph; + + public QueryParametersUnitTestCollection() + { + var builder = new ResourceGraphBuilder(); + builder.AddResource
(); + builder.AddResource(); + builder.AddResource(); + builder.AddResource(); + builder.AddResource(); + _resourceGraph = builder.Build(); + _articleResourceContext = _resourceGraph.GetResourceContext
(); + } + + public ICurrentRequest MockCurrentRequest(ResourceContext requestResource = null) + { + var mock = new Mock(); + + if (requestResource != null) + mock.Setup(m => m.GetRequestResource()).Returns(requestResource); + + return mock.Object; + } + + public IResourceDefinitionProvider MockResourceDefinitionProvider(params (Type, IResourceDefinition)[] rds) + { + var mock = new Mock(); + + foreach (var (type, resourceDefinition) in rds) + mock.Setup(m => m.Get(type)).Returns(resourceDefinition); + + return mock.Object; + } + } +} \ No newline at end of file diff --git a/test/UnitTests/QueryParameters/SortServiceTests.cs b/test/UnitTests/QueryParameters/SortServiceTests.cs new file mode 100644 index 0000000000..4284b89790 --- /dev/null +++ b/test/UnitTests/QueryParameters/SortServiceTests.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Query; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace UnitTests.QueryParameters +{ + public class SortServiceTests : QueryParametersUnitTestCollection + { + public SortService GetService() + { + return new SortService(MockResourceDefinitionProvider(), _resourceGraph, MockCurrentRequest(_articleResourceContext)); + } + + [Fact] + public void Name_SortService_IsCorrect() + { + // Arrange + var filterService = GetService(); + + // Act + var name = filterService.Name; + + // Assert + Assert.Equal("sort", name); + } + + [Theory] + [InlineData("text,,1")] + [InlineData("text,hello,,5")] + [InlineData(",,2")] + public void Parse_InvalidSortQuery_ThrowsJsonApiException(string stringSortQuery) + { + // Arrange + var query = new KeyValuePair($"sort", stringSortQuery); + var sortService = GetService(); + + // Act, assert + var exception = Assert.Throws(() => sortService.Parse(query)); + Assert.Contains("sort", exception.Message); + } + } +} diff --git a/test/UnitTests/QueryParameters/SparseFieldsServiceTests.cs b/test/UnitTests/QueryParameters/SparseFieldsServiceTests.cs new file mode 100644 index 0000000000..a87b9e6927 --- /dev/null +++ b/test/UnitTests/QueryParameters/SparseFieldsServiceTests.cs @@ -0,0 +1,135 @@ +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Query; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace UnitTests.QueryParameters +{ + public class SparseFieldsServiceTests : QueryParametersUnitTestCollection + { + public SparseFieldsService GetService(ResourceContext resourceContext = null) + { + return new SparseFieldsService(_resourceGraph, MockCurrentRequest(resourceContext ?? _articleResourceContext)); + } + + [Fact] + public void Name_SparseFieldsService_IsCorrect() + { + // Arrange + var filterService = GetService(); + + // Act + var name = filterService.Name; + + // Assert + Assert.Equal("fields", name); + } + + [Fact] + public void Parse_ValidSelection_CanParse() + { + // Arrange + const string type = "articles"; + const string attrName = "some-field"; + const string internalAttrName = "SomeField"; + var attribute = new AttrAttribute(attrName) { InternalAttributeName = internalAttrName }; + var idAttribute = new AttrAttribute("id") { InternalAttributeName = "Id" }; + + var query = new KeyValuePair($"fields", new StringValues(attrName)); + + var resourceContext = new ResourceContext + { + ResourceName = type, + Attributes = new List { attribute, idAttribute }, + Relationships = new List() + }; + var service = GetService(resourceContext); + + // Act + service.Parse(query); + var result = service.Get(); + + // Assert + Assert.NotEmpty(result); + Assert.Equal(idAttribute, result.First()); + Assert.Equal(attribute, result[1]); + } + + [Fact] + public void Parse_TypeNameAsNavigation_Throws400ErrorWithRelationshipsOnlyMessage() + { + // Arrange + const string type = "articles"; + const string attrName = "some-field"; + const string internalAttrName = "SomeField"; + var attribute = new AttrAttribute(attrName) { InternalAttributeName = internalAttrName }; + var idAttribute = new AttrAttribute("id") { InternalAttributeName = "Id" }; + + var query = new KeyValuePair($"fields[{type}]", new StringValues(attrName)); + + var resourceContext = new ResourceContext + { + ResourceName = type, + Attributes = new List { attribute, idAttribute }, + Relationships = new List() + }; + var service = GetService(resourceContext); + + // Act, assert + var ex = Assert.Throws(() => service.Parse(query)); + Assert.Contains("relationships only", ex.Message); + } + + [Fact] + public void Parse_DeeplyNestedSelection_Throws400ErrorWithDeeplyNestedMessage() + { + // Arrange + const string type = "articles"; + const string relationship = "author.employer"; + const string attrName = "some-field"; + const string internalAttrName = "SomeField"; + var attribute = new AttrAttribute(attrName) { InternalAttributeName = internalAttrName }; + var idAttribute = new AttrAttribute("id") { InternalAttributeName = "Id" }; + + var query = new KeyValuePair($"fields[{relationship}]", new StringValues(attrName)); + + var resourceContext = new ResourceContext + { + ResourceName = type, + Attributes = new List { attribute, idAttribute }, + Relationships = new List() + }; + var service = GetService(resourceContext); + + // Act, assert + var ex = Assert.Throws(() => service.Parse(query)); + Assert.Contains("deeply nested", ex.Message); + } + + [Fact] + public void Parse_InvalidField_ThrowsJsonApiException() + { + // Arrange + const string type = "articles"; + const string attrName = "dne"; + + var query = new KeyValuePair($"fields[{type}]", new StringValues(attrName)); + + var resourceContext = new ResourceContext + { + ResourceName = type, + Attributes = new List(), + Relationships = new List() + }; + + var service = GetService(resourceContext); + + // Act , assert + var ex = Assert.Throws(() => service.Parse(query)); + Assert.Equal(400, ex.GetStatusCode()); + } + } +} diff --git a/test/UnitTests/ResourceHooks/AffectedEntitiesHelperTests.cs b/test/UnitTests/ResourceHooks/AffectedEntitiesHelperTests.cs index d4f579d121..2d22742625 100644 --- a/test/UnitTests/ResourceHooks/AffectedEntitiesHelperTests.cs +++ b/test/UnitTests/ResourceHooks/AffectedEntitiesHelperTests.cs @@ -40,20 +40,20 @@ public RelationshipDictionaryTests() { FirstToOneAttr = new HasOneAttribute("first-to-one") { - PrincipalType = typeof(Dummy), - DependentType = typeof(ToOne), + LeftType = typeof(Dummy), + RightType = typeof(ToOne), InternalRelationshipName = "FirstToOne" }; SecondToOneAttr = new HasOneAttribute("second-to-one") { - PrincipalType = typeof(Dummy), - DependentType = typeof(ToOne), + LeftType = typeof(Dummy), + RightType = typeof(ToOne), InternalRelationshipName = "SecondToOne" }; ToManyAttr = new HasManyAttribute("to-manies") { - PrincipalType = typeof(Dummy), - DependentType = typeof(ToMany), + LeftType = typeof(Dummy), + RightType = typeof(ToMany), InternalRelationshipName = "ToManies" }; Relationships.Add(FirstToOneAttr, FirstToOnesEntities); @@ -65,30 +65,30 @@ public RelationshipDictionaryTests() [Fact] public void RelationshipsDictionary_GetByRelationships() { - // arrange + // Arrange RelationshipsDictionary relationshipsDictionary = new RelationshipsDictionary(Relationships); - // act + // Act Dictionary> toOnes = relationshipsDictionary.GetByRelationship(); Dictionary> toManies = relationshipsDictionary.GetByRelationship(); Dictionary> notTargeted = relationshipsDictionary.GetByRelationship(); - // assert + // Assert AssertRelationshipDictionaryGetters(relationshipsDictionary, toOnes, toManies, notTargeted); } [Fact] public void RelationshipsDictionary_GetAffected() { - // arrange + // Arrange RelationshipsDictionary relationshipsDictionary = new RelationshipsDictionary(Relationships); - // act + // Act var affectedThroughFirstToOne = relationshipsDictionary.GetAffected(d => d.FirstToOne).ToList(); var affectedThroughSecondToOne = relationshipsDictionary.GetAffected(d => d.SecondToOne).ToList(); var affectedThroughToMany = relationshipsDictionary.GetAffected(d => d.ToManies).ToList(); - // assert + // Assert affectedThroughFirstToOne.ForEach((entitiy) => Assert.Contains(entitiy, FirstToOnesEntities)); affectedThroughSecondToOne.ForEach((entitiy) => Assert.Contains(entitiy, SecondToOnesEntities)); affectedThroughToMany.ForEach((entitiy) => Assert.Contains(entitiy, ToManiesEntities)); @@ -97,10 +97,10 @@ public void RelationshipsDictionary_GetAffected() [Fact] public void EntityHashSet_GetByRelationships() { - // arrange + // Arrange EntityHashSet entities = new EntityHashSet(AllEntities, Relationships); - // act + // Act Dictionary> toOnes = entities.GetByRelationship(); Dictionary> toManies = entities.GetByRelationship(); Dictionary> notTargeted = entities.GetByRelationship(); @@ -118,11 +118,11 @@ public void EntityHashSet_GetByRelationships() [Fact] public void EntityDiff_GetByRelationships() { - // arrange + // Arrange var dbEntities = new HashSet(AllEntities.Select(e => new Dummy { Id = e.Id }).ToList()); DiffableEntityHashSet diffs = new DiffableEntityHashSet(AllEntities, dbEntities, Relationships, null); - // act + // Act Dictionary> toOnes = diffs.GetByRelationship(); Dictionary> toManies = diffs.GetByRelationship(); Dictionary> notTargeted = diffs.GetByRelationship(); @@ -151,7 +151,7 @@ public void EntityDiff_GetByRelationships() [Fact] public void EntityDiff_Loops_Over_Diffs() { - // arrange + // Arrange var dbEntities = new HashSet(AllEntities.Select(e => new Dummy { Id = e.Id })); DiffableEntityHashSet diffs = new DiffableEntityHashSet(AllEntities, dbEntities, Relationships, null); @@ -168,16 +168,16 @@ public void EntityDiff_Loops_Over_Diffs() [Fact] public void EntityDiff_GetAffected_Relationships() { - // arrange + // Arrange var dbEntities = new HashSet(AllEntities.Select(e => new Dummy { Id = e.Id })); DiffableEntityHashSet diffs = new DiffableEntityHashSet(AllEntities, dbEntities, Relationships, null); - // act + // Act var affectedThroughFirstToOne = diffs.GetAffected(d => d.FirstToOne).ToList(); var affectedThroughSecondToOne = diffs.GetAffected(d => d.SecondToOne).ToList(); var affectedThroughToMany = diffs.GetAffected(d => d.ToManies).ToList(); - // assert + // Assert affectedThroughFirstToOne.ForEach((entitiy) => Assert.Contains(entitiy, FirstToOnesEntities)); affectedThroughSecondToOne.ForEach((entitiy) => Assert.Contains(entitiy, SecondToOnesEntities)); affectedThroughToMany.ForEach((entitiy) => Assert.Contains(entitiy, ToManiesEntities)); @@ -186,7 +186,7 @@ public void EntityDiff_GetAffected_Relationships() [Fact] public void EntityDiff_GetAffected_Attributes() { - // arrange + // Arrange var dbEntities = new HashSet(AllEntities.Select(e => new Dummy { Id = e.Id })); var updatedAttributes = new Dictionary> { @@ -194,11 +194,11 @@ public void EntityDiff_GetAffected_Attributes() }; DiffableEntityHashSet diffs = new DiffableEntityHashSet(AllEntities, dbEntities, Relationships, updatedAttributes); - // act + // Act var affectedThroughSomeUpdatedProperty = diffs.GetAffected(d => d.SomeUpdatedProperty).ToList(); var affectedThroughSomeNotUpdatedProperty = diffs.GetAffected(d => d.SomeNotUpdatedProperty).ToList(); - // assert + // Assert Assert.NotEmpty(affectedThroughSomeUpdatedProperty); Assert.Empty(affectedThroughSomeNotUpdatedProperty); } diff --git a/test/UnitTests/ResourceHooks/DiscoveryTests.cs b/test/UnitTests/ResourceHooks/DiscoveryTests.cs index bde024f530..3a37184784 100644 --- a/test/UnitTests/ResourceHooks/DiscoveryTests.cs +++ b/test/UnitTests/ResourceHooks/DiscoveryTests.cs @@ -4,6 +4,7 @@ using Xunit; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; namespace UnitTests.ResourceHooks { @@ -21,9 +22,9 @@ public override void AfterDelete(HashSet entities, ResourcePipeline pipel [Fact] public void Hook_Discovery() { - // arrange & act + // Arrange & act var hookConfig = new HooksDiscovery(); - // assert + // Assert Assert.Contains(ResourceHook.BeforeDelete, hookConfig.ImplementedHooks); Assert.Contains(ResourceHook.AfterDelete, hookConfig.ImplementedHooks); @@ -32,7 +33,7 @@ public void Hook_Discovery() public class AnotherDummy : Identifiable { } public abstract class ResourceDefintionBase : ResourceDefinition where T : class, IIdentifiable { - protected ResourceDefintionBase(IResourceGraph graph) : base(graph) { } + protected ResourceDefintionBase(IResourceGraph resourceGraph) : base(resourceGraph) { } public override IEnumerable BeforeDelete(IEntityHashSet affected, ResourcePipeline pipeline) { return affected; } public override void AfterDelete(HashSet entities, ResourcePipeline pipeline, bool succeeded) { } @@ -45,9 +46,9 @@ public AnotherDummyResourceDefinition() : base(new ResourceGraphBuilder().AddRes [Fact] public void Hook_Discovery_With_Inheritance() { - // arrange & act + // Arrange & act var hookConfig = new HooksDiscovery(); - // assert + // Assert Assert.Contains(ResourceHook.BeforeDelete, hookConfig.ImplementedHooks); Assert.Contains(ResourceHook.AfterDelete, hookConfig.ImplementedHooks); } @@ -60,16 +61,16 @@ public YetAnotherDummyResourceDefinition() : base(new ResourceGraphBuilder().Add public override IEnumerable BeforeDelete(IEntityHashSet affected, ResourcePipeline pipeline) { return affected; } - [LoadDatabaseValues(false)] + [LoaDatabaseValues(false)] public override void AfterDelete(HashSet entities, ResourcePipeline pipeline, bool succeeded) { } } [Fact] - public void LoadDatabaseValues_Attribute_Not_Allowed() + public void LoaDatabaseValues_Attribute_Not_Allowed() { // assert Assert.Throws(() => { - // arrange & act + // Arrange & act var hookConfig = new HooksDiscovery(); }); @@ -94,7 +95,7 @@ public void Multiple_Implementations_Of_ResourceDefinitions() // assert Assert.Throws(() => { - // arrange & act + // Arrange & act var hookConfig = new HooksDiscovery(); }); } diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/AfterCreateTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/AfterCreateTests.cs index dea114facc..d91231280e 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/AfterCreateTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/AfterCreateTests.cs @@ -13,17 +13,16 @@ public class AfterCreateTests : HooksTestsSetup [Fact] public void AfterCreate() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.AfterCreate(todoList, ResourcePipeline.Post); - // assert + // Assert todoResourceMock.Verify(rd => rd.AfterCreate(It.IsAny>(), ResourcePipeline.Post), Times.Once()); ownerResourceMock.Verify(rd => rd.AfterUpdateRelationship(It.IsAny>(), ResourcePipeline.Post), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); @@ -32,17 +31,16 @@ public void AfterCreate() [Fact] public void AfterCreate_Without_Parent_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.AfterCreate(todoList, ResourcePipeline.Post); - // assert + // Assert ownerResourceMock.Verify(rd => rd.AfterUpdateRelationship(It.IsAny>(), ResourcePipeline.Post), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); } @@ -50,17 +48,16 @@ public void AfterCreate_Without_Parent_Hook_Implemented() [Fact] public void AfterCreate_Without_Child_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.AfterCreate(todoList, ResourcePipeline.Post); - // assert + // Assert todoResourceMock.Verify(rd => rd.AfterCreate(It.IsAny>(), ResourcePipeline.Post), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); } @@ -68,17 +65,16 @@ public void AfterCreate_Without_Child_Hook_Implemented() [Fact] public void AfterCreate_Without_Any_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.AfterCreate(todoList, ResourcePipeline.Post); - // assert + // Assert VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); } } diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreateTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreateTests.cs index bc2163df2f..a5438b624d 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreateTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreateTests.cs @@ -13,17 +13,16 @@ public class BeforeCreateTests : HooksTestsSetup [Fact] public void BeforeCreate() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeCreate(It.IsAny>(), ResourcePipeline.Post), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship(It.IsAny>(), It.IsAny>(), ResourcePipeline.Post), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); @@ -32,17 +31,16 @@ public void BeforeCreate() [Fact] public void BeforeCreate_Without_Parent_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeCreate(It.IsAny>(), ResourcePipeline.Post), Times.Never()); ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship(It.IsAny>(), It.IsAny>(), ResourcePipeline.Post), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); @@ -50,34 +48,32 @@ public void BeforeCreate_Without_Parent_Hook_Implemented() [Fact] public void BeforeCreate_Without_Child_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeCreate(It.IsAny>(), ResourcePipeline.Post), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); } [Fact] public void BeforeCreate_Without_Any_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); - // assert + // Assert VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); } } diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreate_WithDbValues_Tests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreate_WithDbValues_Tests.cs index c574de145c..ab9d15ad29 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreate_WithDbValues_Tests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreate_WithDbValues_Tests.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore.Hooks; +using JsonApiDotNetCore.Hooks; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Microsoft.EntityFrameworkCore; @@ -44,16 +44,15 @@ public BeforeCreate_WithDbValues_Tests() [Fact] public void BeforeCreate() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); - // act + // Act hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeCreate(It.Is>((entities) => TodoCheck(entities, description)), ResourcePipeline.Post), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship( It.Is>(ids => PersonIdCheck(ids, personId)), @@ -70,16 +69,15 @@ public void BeforeCreate() [Fact] public void BeforeCreate_Without_Parent_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); - // act + // Act hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); - // assert + // Assert ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship( It.Is>(ids => PersonIdCheck(ids, personId)), It.IsAny>(), @@ -91,16 +89,15 @@ public void BeforeCreate_Without_Parent_Hook_Implemented() [Fact] public void BeforeCreate_Without_Child_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); - // act + // Act hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeCreate(It.Is>((entities) => TodoCheck(entities, description)), ResourcePipeline.Post), Times.Once()); todoResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship( It.Is>(rh => TodoCheckRelationships(rh, description + description)), @@ -112,16 +109,15 @@ public void BeforeCreate_Without_Child_Hook_Implemented() [Fact] public void BeforeCreate_NoImplicit() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooksNoImplicit, ResourceHook.BeforeUpdate); var personDiscovery = SetDiscoverableHooks(targetHooksNoImplicit, ResourceHook.BeforeUpdateRelationship); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); - // act + // Act hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeCreate(It.Is>((entities) => TodoCheck(entities, description)), ResourcePipeline.Post), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship( It.Is>(ids => PersonIdCheck(ids, personId)), @@ -134,16 +130,15 @@ public void BeforeCreate_NoImplicit() [Fact] public void BeforeCreate_NoImplicit_Without_Parent_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooksNoImplicit, ResourceHook.BeforeUpdateRelationship); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); - // act + // Act hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); - // assert + // Assert ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship( It.Is>(ids => PersonIdCheck(ids, personId)), It.IsAny>(), @@ -155,16 +150,15 @@ public void BeforeCreate_NoImplicit_Without_Parent_Hook_Implemented() [Fact] public void BeforeCreate_NoImplicit_Without_Child_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooksNoImplicit, ResourceHook.BeforeUpdate); var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); - // act + // Act hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeCreate(It.Is>((entities) => TodoCheck(entities, description)), ResourcePipeline.Post), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); } diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/AfterDeleteTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/AfterDeleteTests.cs index 051fea3bba..681fa831ef 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/AfterDeleteTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/AfterDeleteTests.cs @@ -13,15 +13,15 @@ public class AfterDeleteTests : HooksTestsSetup [Fact] public void AfterDelete() { - // arrange + // Arrange var discovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var resourceDefinitionMock) = CreateTestObjects(discovery); + var (_, hookExecutor, resourceDefinitionMock) = CreateTestObjects(discovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.AfterDelete(todoList, ResourcePipeline.Delete, It.IsAny()); - // assert + // Assert resourceDefinitionMock.Verify(rd => rd.AfterDelete(It.IsAny>(), ResourcePipeline.Delete, It.IsAny()), Times.Once()); VerifyNoOtherCalls(resourceDefinitionMock); } @@ -29,15 +29,15 @@ public void AfterDelete() [Fact] public void AfterDelete_Without_Any_Hook_Implemented() { - // arrange + // Arrange var discovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var resourceDefinitionMock) = CreateTestObjects(discovery); + (var _, var hookExecutor, var resourceDefinitionMock) = CreateTestObjects(discovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.AfterDelete(todoList, ResourcePipeline.Delete, It.IsAny()); - // assert + // Assert VerifyNoOtherCalls(resourceDefinitionMock); } } diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDeleteTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDeleteTests.cs index 887a322994..eda31fe2ac 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDeleteTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDeleteTests.cs @@ -13,15 +13,15 @@ public class BeforeDeleteTests : HooksTestsSetup [Fact] public void BeforeDelete() { - // arrange + // Arrange var discovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var resourceDefinitionMock) = CreateTestObjects(discovery); + (var _, var hookExecutor, var resourceDefinitionMock) = CreateTestObjects(discovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.BeforeDelete(todoList, ResourcePipeline.Delete); - // assert + // Assert resourceDefinitionMock.Verify(rd => rd.BeforeDelete(It.IsAny>(), It.IsAny()), Times.Once()); resourceDefinitionMock.VerifyNoOtherCalls(); } @@ -29,15 +29,15 @@ public void BeforeDelete() [Fact] public void BeforeDelete_Without_Any_Hook_Implemented() { - // arrange + // Arrange var discovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var resourceDefinitionMock) = CreateTestObjects(discovery); + (var _, var hookExecutor, var resourceDefinitionMock) = CreateTestObjects(discovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.BeforeDelete(todoList, ResourcePipeline.Delete); - // assert + // Assert resourceDefinitionMock.VerifyNoOtherCalls(); } } diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDelete_WithDbValue_Tests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDelete_WithDbValue_Tests.cs index f63adcbd6e..45db4ecb9a 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDelete_WithDbValue_Tests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDelete_WithDbValue_Tests.cs @@ -7,6 +7,7 @@ using System.Linq; using Xunit; + namespace UnitTests.ResourceHooks { public class BeforeDelete_WithDbValues_Tests : HooksTestsSetup @@ -35,39 +36,37 @@ public BeforeDelete_WithDbValues_Tests() [Fact] public void BeforeDelete() { - // arrange + // Arrange var personDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); var todoDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); var passportDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); - (var contextMock, var hookExecutor, var personResourceMock, var todoResourceMock, - var passportResourceMock) = CreateTestObjects(personDiscovery, todoDiscovery, passportDiscovery, repoDbContextOptions: options); + var (_, hookExecutor, personResourceMock, todoResourceMock, passportResourceMock) = CreateTestObjects(personDiscovery, todoDiscovery, passportDiscovery, repoDbContextOptions: options); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.BeforeDelete(new List { person }, ResourcePipeline.Delete); - // assert + // Assert personResourceMock.Verify(rd => rd.BeforeDelete(It.IsAny>(), It.IsAny()), Times.Once()); - todoResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship(It.Is>( rh => CheckImplicitTodos(rh) ), ResourcePipeline.Delete), Times.Once()); - passportResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship(It.Is>( rh => CheckImplicitPassports(rh) ), ResourcePipeline.Delete), Times.Once()); + todoResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship(It.Is>(rh => CheckImplicitTodos(rh)), ResourcePipeline.Delete), Times.Once()); + passportResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship(It.Is>(rh => CheckImplicitPassports(rh)), ResourcePipeline.Delete), Times.Once()); VerifyNoOtherCalls(personResourceMock, todoResourceMock, passportResourceMock); } [Fact] public void BeforeDelete_No_Parent_Hooks() { - // arrange + // Arrange var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var todoDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); var passportDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); - (var contextMock, var hookExecutor, var personResourceMock, var todoResourceMock, - var passportResourceMock) = CreateTestObjects(personDiscovery, todoDiscovery, passportDiscovery, repoDbContextOptions: options); + var (_, hookExecutor, personResourceMock, todoResourceMock, passportResourceMock) = CreateTestObjects(personDiscovery, todoDiscovery, passportDiscovery, repoDbContextOptions: options); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.BeforeDelete(new List { person }, ResourcePipeline.Delete); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship(It.Is>(rh => CheckImplicitTodos(rh)), ResourcePipeline.Delete), Times.Once()); passportResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship(It.Is>(rh => CheckImplicitPassports(rh)), ResourcePipeline.Delete), Times.Once()); VerifyNoOtherCalls(personResourceMock, todoResourceMock, passportResourceMock); @@ -76,18 +75,17 @@ public void BeforeDelete_No_Parent_Hooks() [Fact] public void BeforeDelete_No_Children_Hooks() { - // arrange + // Arrange var personDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var passportDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var personResourceMock, var todoResourceMock, - var passportResourceMock) = CreateTestObjects(personDiscovery, todoDiscovery, passportDiscovery, repoDbContextOptions: options); + var (_, hookExecutor, personResourceMock, todoResourceMock, passportResourceMock) = CreateTestObjects(personDiscovery, todoDiscovery, passportDiscovery, repoDbContextOptions: options); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.BeforeDelete(new List { person }, ResourcePipeline.Delete); - // assert + // Assert personResourceMock.Verify(rd => rd.BeforeDelete(It.IsAny>(), It.IsAny()), Times.Once()); VerifyNoOtherCalls(personResourceMock, todoResourceMock, passportResourceMock); } diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/IdentifiableManyToMany_OnReturnTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/IdentifiableManyToMany_OnReturnTests.cs index fd29857c37..c55ad233c9 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/IdentifiableManyToMany_OnReturnTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/IdentifiableManyToMany_OnReturnTests.cs @@ -14,18 +14,17 @@ public class IdentifiableManyToMany_OnReturnTests : HooksTestsSetup [Fact] public void OnReturn() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); var joinDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var joinResourceMock, var tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateIdentifiableManyToManyData(); + var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); + var (articles, joins, tags) = CreateIdentifiableManyToManyData(); - // act + // Act hookExecutor.OnReturn(articles, ResourcePipeline.Get); - // assert + // Assert articleResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Get), Times.Once()); joinResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(joins).Any()), ResourcePipeline.Get), Times.Once()); tagResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.Get), Times.Once()); @@ -35,18 +34,17 @@ public void OnReturn() [Fact] public void OnReturn_GetRelationship() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); var joinDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var joinResourceMock, var tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateIdentifiableManyToManyData(); + var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); + var (articles, joins, tags) = CreateIdentifiableManyToManyData(); - // act + // Act hookExecutor.OnReturn(articles, ResourcePipeline.GetRelationship); - // assert + // Assert joinResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(joins).Any()), ResourcePipeline.GetRelationship), Times.Once()); tagResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.GetRelationship), Times.Once()); VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); @@ -55,18 +53,17 @@ public void OnReturn_GetRelationship() [Fact] public void OnReturn_Without_Parent_Hook_Implemented() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); var joinDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var joinResourceMock, var tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateIdentifiableManyToManyData(); + var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); + var (articles, joins, tags) = CreateIdentifiableManyToManyData(); - // act + // Act hookExecutor.OnReturn(articles, ResourcePipeline.Get); - // assert + // Assert joinResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(joins).Any()), ResourcePipeline.Get), Times.Once()); tagResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.Get), Times.Once()); VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); @@ -75,19 +72,18 @@ public void OnReturn_Without_Parent_Hook_Implemented() [Fact] public void OnReturn_Without_Children_Hooks_Implemented() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); var joinDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var joinResourceMock, var tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); + var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateIdentifiableManyToManyData(); + var (articles, joins, tags) = CreateIdentifiableManyToManyData(); - // act + // Act hookExecutor.OnReturn(articles, ResourcePipeline.Get); - // assert + // Assert articleResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Get), Times.Once()); tagResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.Get), Times.Once()); VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); @@ -96,18 +92,17 @@ public void OnReturn_Without_Children_Hooks_Implemented() [Fact] public void OnReturn_Without_Grand_Children_Hooks_Implemented() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); var joinDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var joinResourceMock, var tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateIdentifiableManyToManyData(); + var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); + var (articles, joins, tags) = CreateIdentifiableManyToManyData(); - // act + // Act hookExecutor.OnReturn(articles, ResourcePipeline.Get); - // assert + // Assert articleResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Get), Times.Once()); joinResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(joins).Any()), ResourcePipeline.Get), Times.Once()); VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); @@ -116,18 +111,17 @@ public void OnReturn_Without_Grand_Children_Hooks_Implemented() [Fact] public void OnReturn_Without_Any_Descendant_Hooks_Implemented() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); var joinDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var joinResourceMock, var tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateIdentifiableManyToManyData(); + var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); + var (articles, joins, tags) = CreateIdentifiableManyToManyData(); - // act + // Act hookExecutor.OnReturn(articles, ResourcePipeline.Get); - // assert + // Assert articleResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Get), Times.Once()); VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); } @@ -135,15 +129,14 @@ public void OnReturn_Without_Any_Descendant_Hooks_Implemented() [Fact] public void OnReturn_Without_Any_Hook_Implemented() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); var joinDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var joinResourceMock, var tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateIdentifiableManyToManyData(); + var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); + var (articles, joins, tags) = CreateIdentifiableManyToManyData(); - // act + // Act hookExecutor.OnReturn(articles, ResourcePipeline.Get); // asert diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/ManyToMany_OnReturnTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/ManyToMany_OnReturnTests.cs index ad9577c3b5..963b4ce073 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/ManyToMany_OnReturnTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/ManyToMany_OnReturnTests.cs @@ -44,17 +44,16 @@ public class ManyToMany_OnReturnTests : HooksTestsSetup [Fact] public void OnReturn() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateDummyData(); + var (_, _, hookExecutor, articleResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); + var (articles, joins, tags) = CreateDummyData(); - // act + // Act hookExecutor.OnReturn(articles, ResourcePipeline.Get); - // assert + // Assert articleResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Get), Times.Once()); tagResourceMock.Verify(rd => rd.OnReturn(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.Get), Times.Once()); VerifyNoOtherCalls(articleResourceMock, tagResourceMock); @@ -63,14 +62,13 @@ public void OnReturn() [Fact] public void OnReturn_Without_Parent_Hook_Implemented() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateDummyData(); + var (_, _, hookExecutor, articleResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); + var (articles, joins, tags) = CreateDummyData(); - // act + // Act hookExecutor.OnReturn(articles, ResourcePipeline.Get); // asser @@ -81,17 +79,16 @@ public void OnReturn_Without_Parent_Hook_Implemented() [Fact] public void OnReturn_Without_Children_Hooks_Implemented() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateDummyData(); + var (_, _, hookExecutor, articleResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); + var (articles, joins, tags) = CreateDummyData(); - // act + // Act hookExecutor.OnReturn(articles, ResourcePipeline.Get); - // assert + // Assert articleResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Get), Times.Once()); VerifyNoOtherCalls(articleResourceMock, tagResourceMock); } @@ -99,18 +96,17 @@ public void OnReturn_Without_Children_Hooks_Implemented() [Fact] public void OnReturn_Without_Any_Hook_Implemented() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); + var (_, _, hookExecutor, articleResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateDummyData(); + var (articles, joins, tags) = CreateDummyData(); - // act + // Act hookExecutor.OnReturn(articles, ResourcePipeline.Get); - // assert + // Assert VerifyNoOtherCalls(articleResourceMock, tagResourceMock); } } diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/BeforeReadTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/BeforeReadTests.cs index e0eceb2fd5..e776868689 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/BeforeReadTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/BeforeReadTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using JsonApiDotNetCore.Hooks; +using JsonApiDotNetCore.Models; using JsonApiDotNetCoreExample.Models; using Moq; using Xunit; @@ -13,16 +14,16 @@ public class BeforeReadTests : HooksTestsSetup [Fact] public void BeforeRead() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock) = CreateTestObjects(todoDiscovery); + var (iqMock, hookExecutor, todoResourceMock) = CreateTestObjects(todoDiscovery); var todoList = CreateTodoWithOwner(); - contextMock.Setup(c => c.IncludedRelationships).Returns(new List()); - // act + iqMock.Setup(c => c.Get()).Returns(new List>()); + // Act hookExecutor.BeforeRead(ResourcePipeline.Get); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, false, null), Times.Once()); VerifyNoOtherCalls(todoResourceMock); @@ -31,20 +32,19 @@ public void BeforeRead() [Fact] public void BeforeReadWithInclusion() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); + var (iqMock, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); // eg a call on api/todo-items?include=owner,assignee,stake-holders - contextMock.Setup(c => c.IncludedRelationships).Returns(new List() { "owner", "assignee", "stake-holders" }); + iqMock.Setup(c => c.Get()).Returns(GetIncludedRelationshipsChains("owner", "assignee", "stake-holders")); - // act + // Act hookExecutor.BeforeRead(ResourcePipeline.Get); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, false, null), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, true, null), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); @@ -53,21 +53,20 @@ public void BeforeReadWithInclusion() [Fact] public void BeforeReadWithNestedInclusion() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var passportDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock, var passportResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, passportDiscovery); + var (iqMock, hookExecutor, todoResourceMock, ownerResourceMock, passportResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, passportDiscovery); var todoList = CreateTodoWithOwner(); // eg a call on api/todo-items?include=owner.passport,assignee,stake-holders - contextMock.Setup(c => c.IncludedRelationships).Returns(new List() { "owner.passport", "assignee", "stake-holders" }); + iqMock.Setup(c => c.Get()).Returns(GetIncludedRelationshipsChains("owner.passport", "assignee", "stake-holders")); - // act + // Act hookExecutor.BeforeRead(ResourcePipeline.Get); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, false, null), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, true, null), Times.Once()); passportResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, true, null), Times.Once()); @@ -78,21 +77,20 @@ public void BeforeReadWithNestedInclusion() [Fact] public void BeforeReadWithNestedInclusion_No_Parent_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var passportDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock, var passportResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, passportDiscovery); + var (iqMock, hookExecutor, todoResourceMock, ownerResourceMock, passportResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, passportDiscovery); var todoList = CreateTodoWithOwner(); // eg a call on api/todo-items?include=owner.passport,assignee,stake-holders - contextMock.Setup(c => c.IncludedRelationships).Returns(new List() { "owner.passport", "assignee", "stake-holders" }); + iqMock.Setup(c => c.Get()).Returns(GetIncludedRelationshipsChains("owner.passport", "assignee", "stake-holders")); - // act + // Act hookExecutor.BeforeRead(ResourcePipeline.Get); - // assert + // Assert ownerResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, true, null), Times.Once()); passportResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, true, null), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock, passportResourceMock); @@ -101,21 +99,20 @@ public void BeforeReadWithNestedInclusion_No_Parent_Hook_Implemented() [Fact] public void BeforeReadWithNestedInclusion_No_Child_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var passportDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock, var passportResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, passportDiscovery); + var (iqMock, hookExecutor, todoResourceMock, ownerResourceMock, passportResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, passportDiscovery); var todoList = CreateTodoWithOwner(); // eg a call on api/todo-items?include=owner.passport,assignee,stake-holders - contextMock.Setup(c => c.IncludedRelationships).Returns(new List() { "owner.passport", "assignee", "stake-holders" }); + iqMock.Setup(c => c.Get()).Returns(GetIncludedRelationshipsChains("owner.passport", "assignee", "stake-holders")); - // act + // Act hookExecutor.BeforeRead(ResourcePipeline.Get); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, false, null), Times.Once()); passportResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, true, null), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock, passportResourceMock); @@ -124,21 +121,20 @@ public void BeforeReadWithNestedInclusion_No_Child_Hook_Implemented() [Fact] public void BeforeReadWithNestedInclusion_No_Grandchild_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var passportDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock, var passportResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, passportDiscovery); + var (iqMock, hookExecutor, todoResourceMock, ownerResourceMock, passportResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, passportDiscovery); var todoList = CreateTodoWithOwner(); // eg a call on api/todo-items?include=owner.passport,assignee,stake-holders - contextMock.Setup(c => c.IncludedRelationships).Returns(new List() { "owner.passport", "assignee", "stake-holders" }); + iqMock.Setup(c => c.Get()).Returns(GetIncludedRelationshipsChains("owner.passport", "assignee", "stake-holders")); - // act + // Act hookExecutor.BeforeRead(ResourcePipeline.Get); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, false, null), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeRead(ResourcePipeline.Get, true, null), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock, passportResourceMock); @@ -148,21 +144,20 @@ public void BeforeReadWithNestedInclusion_No_Grandchild_Hook_Implemented() [Fact] public void BeforeReadWithNestedInclusion_Without_Any_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var passportDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock, var passportResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, passportDiscovery); + var (iqMock, hookExecutor, todoResourceMock, ownerResourceMock, passportResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, passportDiscovery); var todoList = CreateTodoWithOwner(); // eg a call on api/todo-items?include=owner.passport,assignee,stake-holders - contextMock.Setup(c => c.IncludedRelationships).Returns(new List() { "owner.passport", "assignee", "stake-holders" }); + iqMock.Setup(c => c.Get()).Returns(GetIncludedRelationshipsChains("owner.passport", "assignee", "stake-holders")); - // act + // Act hookExecutor.BeforeRead(ResourcePipeline.Get); - // assert + // Assert VerifyNoOtherCalls(todoResourceMock, ownerResourceMock, passportResourceMock); } } diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/IdentifiableManyToMany_AfterReadTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/IdentifiableManyToMany_AfterReadTests.cs index 1d34524029..e8bb6a5f0a 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/IdentifiableManyToMany_AfterReadTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/IdentifiableManyToMany_AfterReadTests.cs @@ -14,18 +14,17 @@ public class IdentifiableManyToMany_AfterReadTests : HooksTestsSetup [Fact] public void AfterRead() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); var joinDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var joinResourceMock, var tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateIdentifiableManyToManyData(); + var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); + var (articles, joins, tags) = CreateIdentifiableManyToManyData(); - // act + // Act hookExecutor.AfterRead(articles, ResourcePipeline.Get); - // assert + // Assert articleResourceMock.Verify(rd => rd.AfterRead(It.IsAny>(), ResourcePipeline.Get, false), Times.Once()); joinResourceMock.Verify(rd => rd.AfterRead(It.Is>((collection) => !collection.Except(joins).Any()), ResourcePipeline.Get, true), Times.Once()); tagResourceMock.Verify(rd => rd.AfterRead(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.Get, true), Times.Once()); @@ -35,18 +34,17 @@ public void AfterRead() [Fact] public void AfterRead_Without_Parent_Hook_Implemented() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); var joinDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var joinResourceMock, var tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateIdentifiableManyToManyData(); + var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); + var (articles, joins, tags) = CreateIdentifiableManyToManyData(); - // act + // Act hookExecutor.AfterRead(articles, ResourcePipeline.Get); - // assert + // Assert joinResourceMock.Verify(rd => rd.AfterRead(It.Is>((collection) => !collection.Except(joins).Any()), ResourcePipeline.Get, true), Times.Once()); tagResourceMock.Verify(rd => rd.AfterRead(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.Get, true), Times.Once()); VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); @@ -55,20 +53,19 @@ public void AfterRead_Without_Parent_Hook_Implemented() [Fact] public void AfterRead_Without_Children_Hooks_Implemented() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); var joinDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var joinResourceMock, var tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); + var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateIdentifiableManyToManyData(); + var (articles, joins, tags) = CreateIdentifiableManyToManyData(); - // act + // Act hookExecutor.AfterRead(articles, ResourcePipeline.Get); - // assert + // Assert articleResourceMock.Verify(rd => rd.AfterRead(It.IsAny>(), ResourcePipeline.Get, false), Times.Once()); tagResourceMock.Verify(rd => rd.AfterRead(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.Get, true), Times.Once()); VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); @@ -77,18 +74,17 @@ public void AfterRead_Without_Children_Hooks_Implemented() [Fact] public void AfterRead_Without_Grand_Children_Hooks_Implemented() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); var joinDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var joinResourceMock, var tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateIdentifiableManyToManyData(); + var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); + var (articles, joins, tags) = CreateIdentifiableManyToManyData(); - // act + // Act hookExecutor.AfterRead(articles, ResourcePipeline.Get); - // assert + // Assert articleResourceMock.Verify(rd => rd.AfterRead(It.IsAny>(), ResourcePipeline.Get, false), Times.Once()); joinResourceMock.Verify(rd => rd.AfterRead(It.Is>((collection) => !collection.Except(joins).Any()), ResourcePipeline.Get, true), Times.Once()); VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); @@ -97,18 +93,17 @@ public void AfterRead_Without_Grand_Children_Hooks_Implemented() [Fact] public void AfterRead_Without_Any_Descendant_Hooks_Implemented() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); var joinDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var joinResourceMock, var tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateIdentifiableManyToManyData(); + var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); + var (articles, joins, tags) = CreateIdentifiableManyToManyData(); - // act + // Act hookExecutor.AfterRead(articles, ResourcePipeline.Get); - // assert + // Assert articleResourceMock.Verify(rd => rd.AfterRead(It.IsAny>(), ResourcePipeline.Get, false), Times.Once()); VerifyNoOtherCalls(articleResourceMock, joinResourceMock, tagResourceMock); } @@ -116,15 +111,14 @@ public void AfterRead_Without_Any_Descendant_Hooks_Implemented() [Fact] public void AfterRead_Without_Any_Hook_Implemented() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); var joinDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var joinResourceMock, var tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateIdentifiableManyToManyData(); + var (_, hookExecutor, articleResourceMock, joinResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, joinDiscovery, tagDiscovery); + var (articles, joins, tags) = CreateIdentifiableManyToManyData(); - // act + // Act hookExecutor.AfterRead(articles, ResourcePipeline.Get); // asert diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/ManyToMany_AfterReadTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/ManyToMany_AfterReadTests.cs index dcdf81ef94..c3860c935e 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/ManyToMany_AfterReadTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/ManyToMany_AfterReadTests.cs @@ -14,17 +14,16 @@ public class ManyToMany_AfterReadTests : HooksTestsSetup [Fact] public void AfterRead() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateManyToManyData(); + var (_, _, hookExecutor, articleResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); + var (articles, joins, tags) = CreateManyToManyData(); - // act + // Act hookExecutor.AfterRead(articles, ResourcePipeline.Get); - // assert + // Assert articleResourceMock.Verify(rd => rd.AfterRead(It.IsAny>(), ResourcePipeline.Get, false), Times.Once()); tagResourceMock.Verify(rd => rd.AfterRead(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.Get, true), Times.Once()); VerifyNoOtherCalls(articleResourceMock, tagResourceMock); @@ -33,17 +32,16 @@ public void AfterRead() [Fact] public void AfterRead_Without_Parent_Hook_Implemented() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateManyToManyData(); + var (_, _, hookExecutor, articleResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); + var (articles, joins, tags) = CreateManyToManyData(); - // act + // Act hookExecutor.AfterRead(articles, ResourcePipeline.Get); - // assert + // Assert tagResourceMock.Verify(rd => rd.AfterRead(It.Is>((collection) => !collection.Except(tags).Any()), ResourcePipeline.Get, true), Times.Once()); VerifyNoOtherCalls(articleResourceMock, tagResourceMock); } @@ -51,17 +49,16 @@ public void AfterRead_Without_Parent_Hook_Implemented() [Fact] public void AfterRead_Without_Children_Hooks_Implemented() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(targetHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateManyToManyData(); + var (_, _, hookExecutor, articleResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); + var (articles, joins, tags) = CreateManyToManyData(); - // act + // Act hookExecutor.AfterRead(articles, ResourcePipeline.Get); - // assert + // Assert articleResourceMock.Verify(rd => rd.AfterRead(It.IsAny>(), ResourcePipeline.Get, false), Times.Once()); VerifyNoOtherCalls(articleResourceMock, tagResourceMock); } @@ -69,17 +66,16 @@ public void AfterRead_Without_Children_Hooks_Implemented() [Fact] public void AfterRead_Without_Any_Hook_Implemented() { - // arrange + // Arrange var articleDiscovery = SetDiscoverableHooks
(NoHooks, DisableDbValues); var tagDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var articleResourceMock, - var tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); - (var articles, var joins, var tags) = CreateManyToManyData(); + var (_, _, hookExecutor, articleResourceMock, tagResourceMock) = CreateTestObjects(articleDiscovery, tagDiscovery); + var (articles, joins, tags) = CreateManyToManyData(); - // act + // Act hookExecutor.AfterRead(articles, ResourcePipeline.Get); - // assert + // Assert VerifyNoOtherCalls(articleResourceMock, tagResourceMock); } } diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/ScenarioTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/ScenarioTests.cs index c79f633c83..ca1c96db33 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/ScenarioTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/ScenarioTests.cs @@ -13,11 +13,10 @@ public class SameEntityTypeTests : HooksTestsSetup [Fact] public void Entity_Has_Multiple_Relations_To_Same_Type() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); +var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var person1 = new Person(); var todo = new TodoItem { Owner = person1 }; var person2 = new Person { AssignedTodoItems = new List() { todo } }; @@ -26,10 +25,10 @@ public void Entity_Has_Multiple_Relations_To_Same_Type() todo.StakeHolders = new List { person3 }; var todoList = new List() { todo }; - // act + // Act hookExecutor.OnReturn(todoList, ResourcePipeline.Post); - // assert + // Assert todoResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Post), Times.Once()); ownerResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Post), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); @@ -38,18 +37,18 @@ public void Entity_Has_Multiple_Relations_To_Same_Type() [Fact] public void Entity_Has_Cyclic_Relations() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock) = CreateTestObjects(todoDiscovery); + (var contextMock, var hookExecutor, var todoResourceMock) = CreateTestObjects(todoDiscovery); var todo = new TodoItem(); todo.ParentTodoItem = todo; todo.ChildrenTodoItems = new List { todo }; var todoList = new List() { todo }; - // act + // Act hookExecutor.OnReturn(todoList, ResourcePipeline.Post); - // assert + // Assert todoResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Post), Times.Once()); VerifyNoOtherCalls(todoResourceMock); } @@ -57,7 +56,7 @@ public void Entity_Has_Cyclic_Relations() [Fact] public void Entity_Has_Nested_Cyclic_Relations() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); (var contextMock, var hookExecutor, var todoResourceMock) = CreateTestObjects(todoDiscovery); var rootTodo = new TodoItem() { Id = 1 }; @@ -66,14 +65,14 @@ public void Entity_Has_Nested_Cyclic_Relations() var grandChild = new TodoItem() { ParentTodoItem = child, Id = 3 }; child.ChildrenTodoItems = new List { grandChild }; var greatGrandChild = new TodoItem() { ParentTodoItem = grandChild, Id = 4 }; - grandChild.ChildrenTodoItems = new List { greatGrandChild }; - greatGrandChild.ChildrenTodoItems = new List { rootTodo }; + grandChild.ChildrenTodoItems = new List { greatGrandChild }; + greatGrandChild.ChildrenTodoItems = new List { rootTodo }; var todoList = new List() { rootTodo }; - // act + // Act hookExecutor.OnReturn(todoList, ResourcePipeline.Post); - // assert + // Assert todoResourceMock.Verify(rd => rd.OnReturn(It.IsAny>(), ResourcePipeline.Post), Times.Exactly(4)); VerifyNoOtherCalls(todoResourceMock); } diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/AfterUpdateTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/AfterUpdateTests.cs index e8d4f4fd60..148c80cf68 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/AfterUpdateTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/AfterUpdateTests.cs @@ -13,17 +13,16 @@ public class AfterUpdateTests : HooksTestsSetup [Fact] public void AfterUpdate() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.AfterUpdate(todoList, ResourcePipeline.Patch); - // assert + // Assert todoResourceMock.Verify(rd => rd.AfterUpdate(It.IsAny>(), ResourcePipeline.Patch), Times.Once()); ownerResourceMock.Verify(rd => rd.AfterUpdateRelationship(It.IsAny>(), ResourcePipeline.Patch), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); @@ -32,17 +31,16 @@ public void AfterUpdate() [Fact] public void AfterUpdate_Without_Parent_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.AfterUpdate(todoList, ResourcePipeline.Patch); - // assert + // Assert ownerResourceMock.Verify(rd => rd.AfterUpdateRelationship(It.IsAny>(), ResourcePipeline.Patch), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); } @@ -50,17 +48,16 @@ public void AfterUpdate_Without_Parent_Hook_Implemented() [Fact] public void AfterUpdate_Without_Child_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.AfterUpdate(todoList, ResourcePipeline.Patch); - // assert + // Assert todoResourceMock.Verify(rd => rd.AfterUpdate(It.IsAny>(), ResourcePipeline.Patch), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); } @@ -68,17 +65,16 @@ public void AfterUpdate_Without_Child_Hook_Implemented() [Fact] public void AfterUpdate_Without_Any_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.AfterUpdate(todoList, ResourcePipeline.Patch); - // assert + // Assert VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); } } diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdateTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdateTests.cs index 807dc38b18..65047e81c2 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdateTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdateTests.cs @@ -13,17 +13,16 @@ public class BeforeUpdateTests : HooksTestsSetup [Fact] public void BeforeUpdate() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeUpdate(It.IsAny>(), ResourcePipeline.Patch), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship(It.IsAny>(), It.IsAny>(), ResourcePipeline.Patch), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); @@ -32,17 +31,16 @@ public void BeforeUpdate() [Fact] public void BeforeUpdate_Without_Parent_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); - // assert + // Assert ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship(It.IsAny>(), It.IsAny>(), ResourcePipeline.Patch), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); } @@ -50,18 +48,17 @@ public void BeforeUpdate_Without_Parent_Hook_Implemented() [Fact] public void BeforeUpdate_Without_Child_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeUpdate(It.IsAny>(), ResourcePipeline.Patch), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); } @@ -69,17 +66,16 @@ public void BeforeUpdate_Without_Child_Hook_Implemented() [Fact] public void BeforeUpdate_Without_Any_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); - // assert + // Assert VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); } } diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdate_WithDbValues_Tests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdate_WithDbValues_Tests.cs index a13de78f94..05425ffb8e 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdate_WithDbValues_Tests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdate_WithDbValues_Tests.cs @@ -1,6 +1,4 @@ using JsonApiDotNetCore.Hooks; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Microsoft.EntityFrameworkCore; @@ -49,16 +47,15 @@ public BeforeUpdate_WithDbValues_Tests() [Fact] public void BeforeUpdate() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); - // act + // Act hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheckDiff(diff, description)), ResourcePipeline.Patch), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship( It.Is>(ids => PersonIdCheck(ids, personId)), @@ -80,19 +77,18 @@ public void BeforeUpdate() [Fact] public void BeforeUpdate_Deleting_Relationship() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); - var attr = ResourceGraph.Instance.GetContextEntity(typeof(TodoItem)).Relationships.Single(r => r.PublicRelationshipName == "one-to-one-person"); - contextMock.Setup(c => c.RelationshipsToUpdate).Returns(new Dictionary() { { attr, new object() } }); + var (_, ufMock, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); - // act + ufMock.Setup(c => c.Relationships).Returns(_resourceGraph.GetRelationships((TodoItem t) => t.ToOnePerson)); + + // Act var _todoList = new List() { new TodoItem { Id = this.todoList[0].Id } }; hookExecutor.BeforeUpdate(_todoList, ResourcePipeline.Patch); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheckDiff(diff, description)), ResourcePipeline.Patch), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship( It.Is>(rh => PersonCheck(lastName + lastName, rh)), @@ -105,16 +101,15 @@ public void BeforeUpdate_Deleting_Relationship() [Fact] public void BeforeUpdate_Without_Parent_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); - // act + // Act hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); - // assert + // Assert ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship( It.Is>(ids => PersonIdCheck(ids, personId)), It.Is>(rh => PersonCheck(lastName, rh)), @@ -130,16 +125,15 @@ public void BeforeUpdate_Without_Parent_Hook_Implemented() [Fact] public void BeforeUpdate_Without_Child_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); - // act + // Act hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheckDiff(diff, description)), ResourcePipeline.Patch), Times.Once()); todoResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship( It.Is>(rh => TodoCheck(rh, description + description)), @@ -151,16 +145,15 @@ public void BeforeUpdate_Without_Child_Hook_Implemented() [Fact] public void BeforeUpdate_NoImplicit() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooksNoImplicit, ResourceHook.BeforeUpdate); var personDiscovery = SetDiscoverableHooks(targetHooksNoImplicit, ResourceHook.BeforeUpdateRelationship); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); - // act + // Act hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheckDiff(diff, description)), ResourcePipeline.Patch), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship( It.Is>(ids => PersonIdCheck(ids, personId)), @@ -173,16 +166,15 @@ public void BeforeUpdate_NoImplicit() [Fact] public void BeforeUpdate_NoImplicit_Without_Parent_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); var personDiscovery = SetDiscoverableHooks(targetHooksNoImplicit, ResourceHook.BeforeUpdateRelationship); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); - // act + // Act hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); - // assert + // Assert ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship( It.Is>(ids => PersonIdCheck(ids, personId)), It.Is>(rh => PersonCheck(lastName, rh)), @@ -194,16 +186,15 @@ public void BeforeUpdate_NoImplicit_Without_Parent_Hook_Implemented() [Fact] public void BeforeUpdate_NoImplicit_Without_Child_Hook_Implemented() { - // arrange + // Arrange var todoDiscovery = SetDiscoverableHooks(targetHooksNoImplicit, ResourceHook.BeforeUpdate); var personDiscovery = SetDiscoverableHooks(NoHooks, DisableDbValues); - (var contextMock, var hookExecutor, var todoResourceMock, - var ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); + var (_, _, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); - // act + // Act hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); - // assert + // Assert todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheckDiff(diff, description)), ResourcePipeline.Patch), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); } diff --git a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs index 454a2e225f..c341106d0e 100644 --- a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs +++ b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs @@ -5,7 +5,6 @@ using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; using JsonApiDotNetCore.Hooks; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -15,11 +14,16 @@ using System.Collections.Generic; using System.Linq; using Person = JsonApiDotNetCoreExample.Models.Person; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Query; namespace UnitTests.ResourceHooks { public class HooksDummyData { + protected IResourceGraph _resourceGraph; protected ResourceHook[] NoHooks = new ResourceHook[0]; protected ResourceHook[] EnableDbValues = { ResourceHook.BeforeUpdate, ResourceHook.BeforeUpdateRelationship }; protected ResourceHook[] DisableDbValues = new ResourceHook[0]; @@ -32,7 +36,7 @@ public class HooksDummyData protected readonly Faker _passportFaker; public HooksDummyData() { - new ResourceGraphBuilder() + _resourceGraph = new ResourceGraphBuilder() .AddResource() .AddResource() .AddResource() @@ -42,6 +46,8 @@ public HooksDummyData() .AddResource() .Build(); + + _todoFaker = new Faker().Rules((f, i) => i.Id = f.UniqueIndex + 1); _personFaker = new Faker().Rules((f, i) => i.Id = f.UniqueIndex + 1); @@ -134,57 +140,67 @@ protected List CreateTodoWithOwner() public class HooksTestsSetup : HooksDummyData { - protected (Mock, IResourceHookExecutor, Mock>) - CreateTestObjects(IHooksDiscovery discovery = null) - where TMain : class, IIdentifiable + (Mock, Mock, Mock, IJsonApiOptions) CreateMocks() + { + var pfMock = new Mock(); + var ufMock = new Mock(); + var iqsMock = new Mock(); + var optionsMock = new JsonApiOptions { LoaDatabaseValues = false }; + return (ufMock, iqsMock, pfMock, optionsMock); + } + + internal (Mock, ResourceHookExecutor, Mock>) CreateTestObjects(IHooksDiscovery mainDiscovery = null) + where TMain : class, IIdentifiable { // creates the resource definition mock and corresponding ImplementedHooks discovery instance - var mainResource = CreateResourceDefinition(discovery); + var mainResource = CreateResourceDefinition(mainDiscovery); + + // mocking the genericServiceFactory and JsonApiContext and wiring them up. + var (ufMock, iqMock, gpfMock, options) = CreateMocks(); - // mocking the GenericProcessorFactory and JsonApiContext and wiring them up. - (var context, var processorFactory) = CreateContextAndProcessorMocks(); + SetupProcessorFactoryForResourceDefinition(gpfMock, mainResource.Object, mainDiscovery, null); - // wiring up the mocked GenericProcessorFactory to return the correct resource definition - SetupProcessorFactoryForResourceDefinition(processorFactory, mainResource.Object, discovery, context.Object); - var meta = new HookExecutorHelper(context.Object.GenericProcessorFactory, ResourceGraph.Instance, context.Object); - var hookExecutor = new ResourceHookExecutor(meta, context.Object, ResourceGraph.Instance); + var execHelper = new HookExecutorHelper(gpfMock.Object, options); + var traversalHelper = new TraversalHelper(_resourceGraph, ufMock.Object); + var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, iqMock.Object, _resourceGraph); - return (context, hookExecutor, mainResource); + return (iqMock, hookExecutor, mainResource); } - protected (Mock context, IResourceHookExecutor, Mock>, Mock>) + protected (Mock, Mock, IResourceHookExecutor, Mock>, Mock>) CreateTestObjects( - IHooksDiscovery mainDiscovery = null, - IHooksDiscovery nestedDiscovery = null, - DbContextOptions repoDbContextOptions = null + IHooksDiscovery mainDiscovery = null, + IHooksDiscovery nestedDiscovery = null, + DbContextOptions repoDbContextOptions = null ) - where TMain : class, IIdentifiable - where TNested : class, IIdentifiable + where TMain : class, IIdentifiable + where TNested : class, IIdentifiable { // creates the resource definition mock and corresponding for a given set of discoverable hooks var mainResource = CreateResourceDefinition(mainDiscovery); var nestedResource = CreateResourceDefinition(nestedDiscovery); - // mocking the GenericProcessorFactory and JsonApiContext and wiring them up. - (var context, var processorFactory) = CreateContextAndProcessorMocks(); + // mocking the genericServiceFactory and JsonApiContext and wiring them up. + var (ufMock, iqMock, gpfMock, options) = CreateMocks(); var dbContext = repoDbContextOptions != null ? new AppDbContext(repoDbContextOptions) : null; - SetupProcessorFactoryForResourceDefinition(processorFactory, mainResource.Object, mainDiscovery, context.Object, dbContext); - var meta = new HookExecutorHelper(context.Object.GenericProcessorFactory, ResourceGraph.Instance, context.Object); - var hookExecutor = new ResourceHookExecutor(meta, context.Object, ResourceGraph.Instance); + SetupProcessorFactoryForResourceDefinition(gpfMock, mainResource.Object, mainDiscovery, dbContext); + SetupProcessorFactoryForResourceDefinition(gpfMock, nestedResource.Object, nestedDiscovery, dbContext); - SetupProcessorFactoryForResourceDefinition(processorFactory, nestedResource.Object, nestedDiscovery, context.Object, dbContext); + var execHelper = new HookExecutorHelper(gpfMock.Object, options); + var traversalHelper = new TraversalHelper(_resourceGraph, ufMock.Object); + var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, iqMock.Object, _resourceGraph); - return (context, hookExecutor, mainResource, nestedResource); + return (iqMock, ufMock, hookExecutor, mainResource, nestedResource); } - protected (Mock context, IResourceHookExecutor, Mock>, Mock>, Mock>) + protected (Mock, IResourceHookExecutor, Mock>, Mock>, Mock>) CreateTestObjects( - IHooksDiscovery mainDiscovery = null, - IHooksDiscovery firstNestedDiscovery = null, - IHooksDiscovery secondNestedDiscovery = null, - DbContextOptions repoDbContextOptions = null + IHooksDiscovery mainDiscovery = null, + IHooksDiscovery firstNestedDiscovery = null, + IHooksDiscovery secondNestedDiscovery = null, + DbContextOptions repoDbContextOptions = null ) where TMain : class, IIdentifiable where TFirstNested : class, IIdentifiable @@ -195,25 +211,26 @@ public class HooksTestsSetup : HooksDummyData var firstNestedResource = CreateResourceDefinition(firstNestedDiscovery); var secondNestedResource = CreateResourceDefinition(secondNestedDiscovery); - // mocking the GenericProcessorFactory and JsonApiContext and wiring them up. - (var context, var processorFactory) = CreateContextAndProcessorMocks(); + // mocking the genericServiceFactory and JsonApiContext and wiring them up. + var (ufMock, iqMock, gpfMock, options) = CreateMocks(); var dbContext = repoDbContextOptions != null ? new AppDbContext(repoDbContextOptions) : null; - SetupProcessorFactoryForResourceDefinition(processorFactory, mainResource.Object, mainDiscovery, context.Object, dbContext); - var meta = new HookExecutorHelper(context.Object.GenericProcessorFactory, ResourceGraph.Instance, context.Object); - var hookExecutor = new ResourceHookExecutor(meta, context.Object, ResourceGraph.Instance); + SetupProcessorFactoryForResourceDefinition(gpfMock, mainResource.Object, mainDiscovery, dbContext); + SetupProcessorFactoryForResourceDefinition(gpfMock, firstNestedResource.Object, firstNestedDiscovery, dbContext); + SetupProcessorFactoryForResourceDefinition(gpfMock, secondNestedResource.Object, secondNestedDiscovery, dbContext); - SetupProcessorFactoryForResourceDefinition(processorFactory, firstNestedResource.Object, firstNestedDiscovery, context.Object, dbContext); - SetupProcessorFactoryForResourceDefinition(processorFactory, secondNestedResource.Object, secondNestedDiscovery, context.Object, dbContext); + var execHelper = new HookExecutorHelper(gpfMock.Object, options); + var traversalHelper = new TraversalHelper(_resourceGraph, ufMock.Object); + var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, iqMock.Object, _resourceGraph); - return (context, hookExecutor, mainResource, firstNestedResource, secondNestedResource); + return (iqMock, hookExecutor, mainResource, firstNestedResource, secondNestedResource); } - protected IHooksDiscovery SetDiscoverableHooks(ResourceHook[] implementedHooks, params ResourceHook[] enableDbValuesHooks) - where TEntity : class, IIdentifiable + protected IHooksDiscovery SetDiscoverableHooks(ResourceHook[] implementedHooks, params ResourceHook[] enableDbValuesHooks) + where TResource : class, IIdentifiable { - var mock = new Mock>(); + var mock = new Mock>(); mock.Setup(discovery => discovery.ImplementedHooks) .Returns(implementedHooks); @@ -239,8 +256,8 @@ protected void VerifyNoOtherCalls(params dynamic[] resourceMocks) protected DbContextOptions InitInMemoryDb(Action seeder) { var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: "repository_mock") - .Options; + .UseInMemoryDatabase(databaseName: "repository_mock") + .Options; using (var context = new AppDbContext(options)) { @@ -292,30 +309,18 @@ void MockHooks(Mock> resourceDefinition) .Verifiable(); } - (Mock, Mock) CreateContextAndProcessorMocks() - { - var processorFactory = new Mock(); - var context = new Mock(); - context.Setup(c => c.GenericProcessorFactory).Returns(processorFactory.Object); - context.Setup(c => c.Options).Returns(new JsonApiOptions { LoadDatabaseValues = false }); - context.Setup(c => c.ResourceGraph).Returns(ResourceGraph.Instance); - - return (context, processorFactory); - } - void SetupProcessorFactoryForResourceDefinition( - Mock processorFactory, + Mock processorFactory, IResourceHookContainer modelResource, IHooksDiscovery discovery, - IJsonApiContext apiContext, AppDbContext dbContext = null ) where TModel : class, IIdentifiable { - processorFactory.Setup(c => c.GetProcessor(typeof(ResourceDefinition<>), typeof(TModel))) + processorFactory.Setup(c => c.Get(typeof(ResourceDefinition<>), typeof(TModel))) .Returns(modelResource); - processorFactory.Setup(c => c.GetProcessor(typeof(IHooksDiscovery<>), typeof(TModel))) + processorFactory.Setup(c => c.Get(typeof(IHooksDiscovery<>), typeof(TModel))) .Returns(discovery); if (dbContext != null) @@ -323,9 +328,10 @@ void SetupProcessorFactoryForResourceDefinition( var idType = TypeHelper.GetIdentifierType(); if (idType == typeof(int)) { - IEntityReadRepository repo = CreateTestRepository(dbContext, apiContext); - processorFactory.Setup(c => c.GetProcessor>(typeof(IEntityReadRepository<,>), typeof(TModel), typeof(int))).Returns(repo); - } else + IResourceReadRepository repo = CreateTestRepository(dbContext); + processorFactory.Setup(c => c.Get>(typeof(IResourceReadRepository<,>), typeof(TModel), typeof(int))).Returns(repo); + } + else { throw new TypeLoadException("Test not set up properly"); } @@ -333,13 +339,12 @@ void SetupProcessorFactoryForResourceDefinition( } } - IEntityReadRepository CreateTestRepository( - AppDbContext dbContext, - IJsonApiContext apiContext + IResourceReadRepository CreateTestRepository( + AppDbContext dbContext ) where TModel : class, IIdentifiable { IDbContextResolver resolver = CreateTestDbResolver(dbContext); - return new DefaultEntityRepository(apiContext, resolver); + return new DefaultResourceRepository(null, resolver, null, null, null); } IDbContextResolver CreateTestDbResolver(AppDbContext dbContext) where TModel : class, IIdentifiable @@ -351,7 +356,7 @@ IDbContextResolver CreateTestDbResolver(AppDbContext dbContext) where TM void ResolveInverseRelationships(AppDbContext context) { - new InverseRelationships(ResourceGraph.Instance, new DbContextResolver(context)).Resolve(); + new InverseRelationships(_resourceGraph, new DbContextResolver(context)).Resolve(); } Mock> CreateResourceDefinition @@ -363,6 +368,30 @@ Mock> CreateResourceDefinition MockHooks(resourceDefinition); return resourceDefinition; } + + protected List> GetIncludedRelationshipsChains(params string[] chains) + { + var parsedChains = new List>(); + + foreach (var chain in chains) + parsedChains.Add(GetIncludedRelationshipsChain(chain)); + + return parsedChains; + } + + protected List GetIncludedRelationshipsChain(string chain) + { + var parsedChain = new List(); + var resourceContext = _resourceGraph.GetResourceContext(); + var splittedPath = chain.Split(QueryConstants.DOT); + foreach (var requestedRelationship in splittedPath) + { + var relationship = resourceContext.Relationships.Single(r => r.PublicRelationshipName == requestedRelationship); + parsedChain.Add(relationship); + resourceContext = _resourceGraph.GetResourceContext(relationship.RightType); + } + return parsedChain; + } } } diff --git a/test/UnitTests/Serialization/Client/RequestSerializerTests.cs b/test/UnitTests/Serialization/Client/RequestSerializerTests.cs new file mode 100644 index 0000000000..549b769962 --- /dev/null +++ b/test/UnitTests/Serialization/Client/RequestSerializerTests.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Client; +using Xunit; +using UnitTests.TestModels; +using Person = UnitTests.TestModels.Person; + +namespace UnitTests.Serialization.Client +{ + public class RequestSerializerTests : SerializerTestsSetup + { + private readonly RequestSerializer _serializer; + + public RequestSerializerTests() + { + var builder = new ResourceObjectBuilder(_resourceGraph, new ResourceObjectBuilderSettings()); + _serializer = new RequestSerializer(_resourceGraph, builder); + } + + [Fact] + public void SerializeSingle_ResourceWithDefaultTargetFields_CanBuild() + { + // Arrange + var entity = new TestResource() { Id = 1, StringField = "value", NullableIntField = 123 }; + + // Act + string serialized = _serializer.Serialize(entity); + + // Assert + var expectedFormatted = + @"{ + ""data"":{ + ""type"":""test-resource"", + ""id"":""1"", + ""attributes"":{ + ""string-field"":""value"", + ""date-time-field"":""0001-01-01T00:00:00"", + ""nullable-date-time-field"":null, + ""int-field"":0, + ""nullable-int-field"":123, + ""guid-field"":""00000000-0000-0000-0000-000000000000"", + ""complex-field"":null, + ""immutable"":null + } + } + }"; + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeSingle_ResourceWithTargetedSetAttributes_CanBuild() + { + // Arrange + var entity = new TestResource() { Id = 1, StringField = "value", NullableIntField = 123 }; + _serializer.SetAttributesToSerialize(tr => tr.StringField); + + // Act + string serialized = _serializer.Serialize(entity); + + // Assert + var expectedFormatted = + @"{ + ""data"":{ + ""type"":""test-resource"", + ""id"":""1"", + ""attributes"":{ + ""string-field"":""value"" + } + } + }"; + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeSingle_NoIdWithTargetedSetAttributes_CanBuild() + { + // Arrange + var entityNoId = new TestResource() { Id = 0, StringField = "value", NullableIntField = 123 }; + _serializer.SetAttributesToSerialize(tr => tr.StringField); + + // Act + string serialized = _serializer.Serialize(entityNoId); + + // Assert + var expectedFormatted = + @"{ + ""data"":{ + ""type"":""test-resource"", + ""attributes"":{ + ""string-field"":""value"" + } + } + }"; + + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeSingle_ResourceWithoutTargetedAttributes_CanBuild() + { + // Arrange + var entity = new TestResource() { Id = 1, StringField = "value", NullableIntField = 123 }; + _serializer.SetAttributesToSerialize(tr => new { }); + + // Act + string serialized = _serializer.Serialize(entity); + + // Assert + var expectedFormatted = + @"{ + ""data"":{ + ""type"":""test-resource"", + ""id"":""1"" + } + }"; + + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeSingle_ResourceWithTargetedRelationships_CanBuild() + { + // Arrange + var entityWithRelationships = new MultipleRelationshipsPrincipalPart + { + PopulatedToOne = new OneToOneDependent { Id = 10 }, + PopulatedToManies = new List { new OneToManyDependent { Id = 20 } } + }; + _serializer.SetRelationshipsToSerialize(tr => new { tr.EmptyToOne, tr.EmptyToManies, tr.PopulatedToOne, tr.PopulatedToManies }); + + // Act + string serialized = _serializer.Serialize(entityWithRelationships); + // Assert + var expectedFormatted = + @"{ + ""data"":{ + ""type"":""multi-principals"", + ""attributes"":{ + ""attribute-member"":null + }, + ""relationships"":{ + ""empty-to-one"":{ + ""data"":null + }, + ""empty-to-manies"":{ + ""data"":[ ] + }, + ""populated-to-one"":{ + ""data"":{ + ""type"":""one-to-one-dependents"", + ""id"":""10"" + } + }, + ""populated-to-manies"":{ + ""data"":[ + { + ""type"":""one-to-many-dependents"", + ""id"":""20"" + } + ] + } + } + } + }"; + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeMany_ResourcesWithTargetedAttributes_CanBuild() + { + // Arrange + var entities = new List + { + new TestResource() { Id = 1, StringField = "value1", NullableIntField = 123 }, + new TestResource() { Id = 2, StringField = "value2", NullableIntField = 123 } + }; + _serializer.SetAttributesToSerialize(tr => tr.StringField); + + // Act + string serialized = _serializer.Serialize(entities); + + // Assert + var expectedFormatted = + @"{ + ""data"":[ + { + ""type"":""test-resource"", + ""id"":""1"", + ""attributes"":{ + ""string-field"":""value1"" + } + }, + { + ""type"":""test-resource"", + ""id"":""2"", + ""attributes"":{ + ""string-field"":""value2"" + } + } + ] + }"; + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeSingle_Null_CanBuild() + { + // Arrange + _serializer.SetAttributesToSerialize(tr => tr.StringField); + + // Act + IIdentifiable obj = null; + string serialized = _serializer.Serialize(obj); + + // Assert + var expectedFormatted = + @"{ + ""data"":null + }"; + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeMany_EmptyList_CanBuild() + { + // Arrange + var entities = new List { }; + _serializer.SetAttributesToSerialize(tr => tr.StringField); + + // Act + string serialized = _serializer.Serialize(entities); + + // Assert + var expectedFormatted = + @"{ + ""data"":[] + }"; + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + Assert.Equal(expected, serialized); + } + } +} diff --git a/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs b/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs new file mode 100644 index 0000000000..f798e77388 --- /dev/null +++ b/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs @@ -0,0 +1,338 @@ +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.Links; +using JsonApiDotNetCore.Serialization.Client; +using Newtonsoft.Json; +using Xunit; +using UnitTests.TestModels; + +namespace UnitTests.Serialization.Client +{ + public class ResponseDeserializerTests : DeserializerTestsSetup + { + private readonly Dictionary _linkValues = new Dictionary(); + private readonly ResponseDeserializer _deserializer; + + public ResponseDeserializerTests() + { + _deserializer = new ResponseDeserializer(_resourceGraph); + _linkValues.Add("self", "http://example.com/articles"); + _linkValues.Add("next", "http://example.com/articles?page[offset]=2"); + _linkValues.Add("last", "http://example.com/articles?page[offset]=10"); + } + + [Fact] + public void DeserializeSingle_EmptyResponseWithMeta_CanDeserialize() + { + // Arrange + var content = new Document + { + Meta = new Dictionary { { "foo", "bar" } } + }; + var body = JsonConvert.SerializeObject(content); + + // Act + var result = _deserializer.DeserializeSingle(body); + + // Assert + Assert.Null(result.Data); + Assert.NotNull(result.Meta); + Assert.Equal("bar", result.Meta["foo"]); + } + + [Fact] + public void DeserializeSingle_EmptyResponseWithTopLevelLinks_CanDeserialize() + { + // Arrange + var content = new Document + { + Links = new TopLevelLinks { Self = _linkValues["self"], Next = _linkValues["next"], Last = _linkValues["last"] } + }; + var body = JsonConvert.SerializeObject(content); + + // Act + var result = _deserializer.DeserializeSingle(body); + + // Assert + Assert.Null(result.Data); + Assert.NotNull(result.Links); + TopLevelLinks links = (TopLevelLinks)result.Links; + Assert.Equal(_linkValues["self"], links.Self); + Assert.Equal(_linkValues["next"], links.Next); + Assert.Equal(_linkValues["last"], links.Last); + } + + [Fact] + public void DeserializeList_EmptyResponseWithTopLevelLinks_CanDeserialize() + { + // Arrange + var content = new Document + { + Links = new TopLevelLinks { Self = _linkValues["self"], Next = _linkValues["next"], Last = _linkValues["last"] }, + Data = new List() + }; + var body = JsonConvert.SerializeObject(content); + + // Act + var result = _deserializer.DeserializeList(body); + + // Assert + Assert.Empty(result.Data); + Assert.NotNull(result.Links); + TopLevelLinks links = (TopLevelLinks)result.Links; + Assert.Equal(_linkValues["self"], links.Self); + Assert.Equal(_linkValues["next"], links.Next); + Assert.Equal(_linkValues["last"], links.Last); + } + + [Fact] + public void DeserializeSingle_ResourceWithAttributes_CanDeserialize() + { + // Arrange + var content = CreateTestResourceDocument(); + var body = JsonConvert.SerializeObject(content); + + // Act + var result = _deserializer.DeserializeSingle(body); + var entity = result.Data; + + // Assert + Assert.Null(result.Links); + Assert.Null(result.Meta); + Assert.Equal(1, entity.Id); + Assert.Equal(content.SingleData.Attributes["string-field"], entity.StringField); + } + + [Fact] + public void DeserializeSingle_MultipleDependentRelationshipsWithIncluded_CanDeserialize() + { + // Arrange + var content = CreateDocumentWithRelationships("multi-principals"); + content.SingleData.Relationships.Add("populated-to-one", CreateRelationshipData("one-to-one-dependents")); + content.SingleData.Relationships.Add("populated-to-manies", CreateRelationshipData("one-to-many-dependents", isToManyData: true)); + content.SingleData.Relationships.Add("empty-to-one", CreateRelationshipData()); + content.SingleData.Relationships.Add("empty-to-manies", CreateRelationshipData(isToManyData: true)); + var toOneAttributeValue = "populated-to-one member content"; + var toManyAttributeValue = "populated-to-manies member content"; + content.Included = new List() + { + new ResourceObject() + { + Type = "one-to-one-dependents", + Id = "10", + Attributes = new Dictionary() { {"attribute-member", toOneAttributeValue } } + }, + new ResourceObject() + { + Type = "one-to-many-dependents", + Id = "10", + Attributes = new Dictionary() { {"attribute-member", toManyAttributeValue } } + } + }; + var body = JsonConvert.SerializeObject(content); + + // Act + var result = _deserializer.DeserializeSingle(body); + var entity = result.Data; + + // Assert + Assert.Equal(1, entity.Id); + Assert.NotNull(entity.PopulatedToOne); + Assert.Equal(toOneAttributeValue, entity.PopulatedToOne.AttributeMember); + Assert.Equal(toManyAttributeValue, entity.PopulatedToManies.First().AttributeMember); + Assert.NotNull(entity.PopulatedToManies); + Assert.NotNull(entity.EmptyToManies); + Assert.Empty(entity.EmptyToManies); + Assert.Null(entity.EmptyToOne); + } + + [Fact] + public void DeserializeSingle_MultiplePrincipalRelationshipsWithIncluded_CanDeserialize() + { + // Arrange + var content = CreateDocumentWithRelationships("multi-dependents"); + content.SingleData.Relationships.Add("populated-to-one", CreateRelationshipData("one-to-one-principals")); + content.SingleData.Relationships.Add("populated-to-many", CreateRelationshipData("one-to-many-principals")); + content.SingleData.Relationships.Add("empty-to-one", CreateRelationshipData()); + content.SingleData.Relationships.Add("empty-to-many", CreateRelationshipData()); + var toOneAttributeValue = "populated-to-one member content"; + var toManyAttributeValue = "populated-to-manies member content"; + content.Included = new List() + { + new ResourceObject() + { + Type = "one-to-one-principals", + Id = "10", + Attributes = new Dictionary() { {"attribute-member", toOneAttributeValue } } + }, + new ResourceObject() + { + Type = "one-to-many-principals", + Id = "10", + Attributes = new Dictionary() { {"attribute-member", toManyAttributeValue } } + } + }; + var body = JsonConvert.SerializeObject(content); + + // Act + var result = _deserializer.DeserializeSingle(body); + var entity = result.Data; + + // Assert + Assert.Equal(1, entity.Id); + Assert.NotNull(entity.PopulatedToOne); + Assert.Equal(toOneAttributeValue, entity.PopulatedToOne.AttributeMember); + Assert.Equal(toManyAttributeValue, entity.PopulatedToMany.AttributeMember); + Assert.NotNull(entity.PopulatedToMany); + Assert.Null(entity.EmptyToMany); + Assert.Null(entity.EmptyToOne); + } + + [Fact] + public void DeserializeSingle_NestedIncluded_CanDeserialize() + { + // Arrange + var content = CreateDocumentWithRelationships("multi-principals"); + content.SingleData.Relationships.Add("populated-to-manies", CreateRelationshipData("one-to-many-dependents", isToManyData: true)); + var toManyAttributeValue = "populated-to-manies member content"; + var nestedIncludeAttributeValue = "nested include member content"; + content.Included = new List() + { + new ResourceObject() + { + Type = "one-to-many-dependents", + Id = "10", + Attributes = new Dictionary() { {"attribute-member", toManyAttributeValue } }, + Relationships = new Dictionary { { "principal", CreateRelationshipData("one-to-many-principals") } } + }, + new ResourceObject() + { + Type = "one-to-many-principals", + Id = "10", + Attributes = new Dictionary() { {"attribute-member", nestedIncludeAttributeValue } } + } + }; + var body = JsonConvert.SerializeObject(content); + + // Act + var result = _deserializer.DeserializeSingle(body); + var entity = result.Data; + + // Assert + Assert.Equal(1, entity.Id); + Assert.Null(entity.PopulatedToOne); + Assert.Null(entity.EmptyToManies); + Assert.Null(entity.EmptyToOne); + Assert.NotNull(entity.PopulatedToManies); + var includedEntity = entity.PopulatedToManies.First(); + Assert.Equal(toManyAttributeValue, includedEntity.AttributeMember); + var nestedIncludedEntity = includedEntity.Principal; + Assert.Equal(nestedIncludeAttributeValue, nestedIncludedEntity.AttributeMember); + } + + + [Fact] + public void DeserializeSingle_DeeplyNestedIncluded_CanDeserialize() + { + // Arrange + var content = CreateDocumentWithRelationships("multi-principals"); + content.SingleData.Relationships.Add("multi", CreateRelationshipData("multi-principals")); + var includedAttributeValue = "multi member content"; + var nestedIncludedAttributeValue = "nested include member content"; + var deeplyNestedIncludedAttributeValue = "deeply nested member content"; + content.Included = new List() + { + new ResourceObject() + { + Type = "multi-principals", + Id = "10", + Attributes = new Dictionary() { {"attribute-member", includedAttributeValue } }, + Relationships = new Dictionary { { "populated-to-manies", CreateRelationshipData("one-to-many-dependents", isToManyData: true) } } + }, + new ResourceObject() + { + Type = "one-to-many-dependents", + Id = "10", + Attributes = new Dictionary() { {"attribute-member", nestedIncludedAttributeValue } }, + Relationships = new Dictionary { { "principal", CreateRelationshipData("one-to-many-principals") } } + }, + new ResourceObject() + { + Type = "one-to-many-principals", + Id = "10", + Attributes = new Dictionary() { {"attribute-member", deeplyNestedIncludedAttributeValue } } + }, + }; + var body = JsonConvert.SerializeObject(content); + + // Act + var result = _deserializer.DeserializeSingle(body); + var entity = result.Data; + + // Assert + Assert.Equal(1, entity.Id); + var included = entity.Multi; + Assert.Equal(10, included.Id); + Assert.Equal(includedAttributeValue, included.AttributeMember); + var nestedIncluded = included.PopulatedToManies.First(); + Assert.Equal(10, nestedIncluded.Id); + Assert.Equal(nestedIncludedAttributeValue, nestedIncluded.AttributeMember); + var deeplyNestedIncluded = nestedIncluded.Principal; + Assert.Equal(10, deeplyNestedIncluded.Id); + Assert.Equal(deeplyNestedIncludedAttributeValue, deeplyNestedIncluded.AttributeMember); + } + + + [Fact] + public void DeserializeList_DeeplyNestedIncluded_CanDeserialize() + { + // Arrange + var content = new Document { Data = new List { CreateDocumentWithRelationships("multi-principals").SingleData } }; + content.ManyData[0].Relationships.Add("multi", CreateRelationshipData("multi-principals")); + var includedAttributeValue = "multi member content"; + var nestedIncludedAttributeValue = "nested include member content"; + var deeplyNestedIncludedAttributeValue = "deeply nested member content"; + content.Included = new List() + { + new ResourceObject() + { + Type = "multi-principals", + Id = "10", + Attributes = new Dictionary() { {"attribute-member", includedAttributeValue } }, + Relationships = new Dictionary { { "populated-to-manies", CreateRelationshipData("one-to-many-dependents", isToManyData: true) } } + }, + new ResourceObject() + { + Type = "one-to-many-dependents", + Id = "10", + Attributes = new Dictionary() { {"attribute-member", nestedIncludedAttributeValue } }, + Relationships = new Dictionary { { "principal", CreateRelationshipData("one-to-many-principals") } } + }, + new ResourceObject() + { + Type = "one-to-many-principals", + Id = "10", + Attributes = new Dictionary() { {"attribute-member", deeplyNestedIncludedAttributeValue } } + }, + }; + var body = JsonConvert.SerializeObject(content); + + // Act + var result = _deserializer.DeserializeList(body); + var entity = result.Data.First(); + + // Assert + Assert.Equal(1, entity.Id); + var included = entity.Multi; + Assert.Equal(10, included.Id); + Assert.Equal(includedAttributeValue, included.AttributeMember); + var nestedIncluded = included.PopulatedToManies.First(); + Assert.Equal(10, nestedIncluded.Id); + Assert.Equal(nestedIncludedAttributeValue, nestedIncluded.AttributeMember); + var deeplyNestedIncluded = nestedIncluded.Principal; + Assert.Equal(10, deeplyNestedIncluded.Id); + Assert.Equal(deeplyNestedIncludedAttributeValue, deeplyNestedIncluded.AttributeMember); + } + } +} diff --git a/test/UnitTests/Serialization/Common/DocumentBuilderTests.cs b/test/UnitTests/Serialization/Common/DocumentBuilderTests.cs new file mode 100644 index 0000000000..78fb2f53ef --- /dev/null +++ b/test/UnitTests/Serialization/Common/DocumentBuilderTests.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization; +using Moq; +using Xunit; +using UnitTests.TestModels; +using Person = UnitTests.TestModels.Person; +namespace UnitTests.Serialization.Serializer +{ + public class BaseDocumentBuilderTests : SerializerTestsSetup + { + private readonly TestDocumentBuilder _builder; + + public BaseDocumentBuilderTests() + { + var mock = new Mock(); + mock.Setup(m => m.Build(It.IsAny(), It.IsAny>(), It.IsAny>())).Returns(new ResourceObject()); + _builder = new TestDocumentBuilder(mock.Object, _resourceGraph); + } + + + [Fact] + public void EntityToDocument_NullEntity_CanBuild() + { + // Arrange + TestResource entity = null; + + // Act + var document = _builder.Build(entity, null, null); + + // Assert + Assert.Null(document.Data); + Assert.False(document.IsPopulated); + } + + + [Fact] + public void EntityToDocument_EmptyList_CanBuild() + { + // Arrange + var entities = new List(); + + // Act + var document = _builder.Build(entities, null, null); + + // Assert + Assert.NotNull(document.Data); + Assert.Empty(document.ManyData); + } + + + [Fact] + public void EntityToDocument_SingleEntity_CanBuild() + { + // Arrange + IIdentifiable dummy = new Identifiable(); + + // Act + var document = _builder.Build(dummy, null, null); + + // Assert + Assert.NotNull(document.Data); + Assert.True(document.IsPopulated); + } + + [Fact] + public void EntityToDocument_EntityList_CanBuild() + { + // Arrange + var entities = new List() { new Identifiable(), new Identifiable() }; + + // Act + var document = _builder.Build(entities, null, null); + var data = (List)document.Data; + + // Assert + Assert.Equal(2, data.Count); + } + } +} diff --git a/test/UnitTests/Serialization/Common/DocumentParserTests.cs b/test/UnitTests/Serialization/Common/DocumentParserTests.cs new file mode 100644 index 0000000000..562fb7d2c5 --- /dev/null +++ b/test/UnitTests/Serialization/Common/DocumentParserTests.cs @@ -0,0 +1,374 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Models; +using Newtonsoft.Json; +using Xunit; +using UnitTests.TestModels; +using Person = UnitTests.TestModels.Person; + +namespace UnitTests.Serialization.Deserializer +{ + public class BaseDocumentParserTests : DeserializerTestsSetup + { + private readonly TestDocumentParser _deserializer; + + public BaseDocumentParserTests() + { + _deserializer = new TestDocumentParser(_resourceGraph); + } + + [Fact] + public void DeserializeResourceIdentifiers_SingleData_CanDeserialize() + { + // arange + var content = new Document + { + Data = new ResourceObject + { + Type = "test-resource", + Id = "1", + } + }; + var body = JsonConvert.SerializeObject(content); + + // Act + var result = (TestResource)_deserializer.Deserialize(body); + + // Assert + Assert.Equal(1, result.Id); + } + + [Fact] + public void DeserializeResourceIdentifiers_EmptySingleData_CanDeserialize() + { + // arange + var content = new Document { }; + var body = JsonConvert.SerializeObject(content); + + // Act + var result = _deserializer.Deserialize(body); + + // Arrange + Assert.Null(result); + } + + [Fact] + public void DeserializeResourceIdentifiers_ArrayData_CanDeserialize() + { + // arange + var content = new Document + { + Data = new List + { + new ResourceObject + { + Type = "test-resource", + Id = "1", + } + } + }; + var body = JsonConvert.SerializeObject(content); + + // Act + var result = (List)_deserializer.Deserialize(body); + + // Assert + Assert.Equal("1", result.First().StringId); + } + + [Fact] + public void DeserializeResourceIdentifiers_EmptyArrayData_CanDeserialize() + { + var content = new Document { Data = new List { } }; + var body = JsonConvert.SerializeObject(content); + + // Act + var result = (IList)_deserializer.Deserialize(body); + + // Assert + Assert.Empty(result); + } + + [Theory] + [InlineData("string-field", "some string")] + [InlineData("string-field", null)] + [InlineData("int-field", null, true)] + [InlineData("int-field", 1)] + [InlineData("int-field", "1")] + [InlineData("nullable-int-field", null)] + [InlineData("nullable-int-field", "1")] + [InlineData("guid-field", "bad format", true)] + [InlineData("guid-field", "1a68be43-cc84-4924-a421-7f4d614b7781")] + [InlineData("date-time-field", "9/11/2019 11:41:40 AM")] + [InlineData("date-time-field", null, true)] + [InlineData("nullable-date-time-field", null)] + public void DeserializeAttributes_VariousDataTypes_CanDeserialize(string member, object value, bool expectError = false) + { + // Arrange + var content = new Document + { + Data = new ResourceObject + { + Type = "test-resource", + Id = "1", + Attributes = new Dictionary + { + { member, value } + } + } + }; + var body = JsonConvert.SerializeObject(content); + + // Act, assert + if (expectError) + { + Assert.ThrowsAny(() => _deserializer.Deserialize(body)); + return; + } + + // Act + var entity = (TestResource)_deserializer.Deserialize(body); + + // Assert + var pi = _resourceGraph.GetResourceContext("test-resource").Attributes.Single(attr => attr.PublicAttributeName == member).PropertyInfo; + var deserializedValue = pi.GetValue(entity); + + if (member == "int-field") + { + Assert.Equal(1, deserializedValue); + } + else if (member == "nullable-int-field" && value == null) + { + Assert.Null(deserializedValue); + } + else if (member == "nullable-int-field" && (string)value == "1") + { + Assert.Equal(1, deserializedValue); + } + else if (member == "guid-field") + { + Assert.Equal(deserializedValue, Guid.Parse("1a68be43-cc84-4924-a421-7f4d614b7781")); + } + else if (member == "date-time-field") + { + Assert.Equal(deserializedValue, DateTime.Parse("9/11/2019 11:41:40 AM")); + } + else + { + Assert.Equal(value, deserializedValue); + } + } + + [Fact] + public void DeserializeAttributes_ComplexType_CanDeserialize() + { + // Arrange + var content = new Document + { + Data = new ResourceObject + { + Type = "test-resource", + Id = "1", + Attributes = new Dictionary + { + { "complex-field", new Dictionary { {"compoundName", "testName" } } } // this is not right + } + } + }; + var body = JsonConvert.SerializeObject(content); + + // Act + var result = (TestResource)_deserializer.Deserialize(body); + + // Assert + Assert.NotNull(result.ComplexField); + Assert.Equal("testName", result.ComplexField.CompoundName); + } + + [Fact] + public void DeserializeAttributes_ComplexListType_CanDeserialize() + { + // Arrange + var content = new Document + { + Data = new ResourceObject + { + Type = "test-resource-with-list", + Id = "1", + Attributes = new Dictionary + { + { "complex-fields", new [] { new Dictionary { {"compoundName", "testName" } } } } + } + } + }; + var body = JsonConvert.SerializeObject(content); + + + // Act + var result = (TestResourceWithList)_deserializer.Deserialize(body); + + // Assert + Assert.NotNull(result.ComplexFields); + Assert.NotEmpty(result.ComplexFields); + Assert.Equal("testName", result.ComplexFields[0].CompoundName); + } + + [Fact] + public void DeserializeRelationships_EmptyOneToOneDependent_NavigationPropertyIsNull() + { + // Arrange + var content = CreateDocumentWithRelationships("one-to-one-principals", "dependent"); + var body = JsonConvert.SerializeObject(content); + + // Act + var result = (OneToOnePrincipal)_deserializer.Deserialize(body); + + // Assert + Assert.Equal(1, result.Id); + Assert.Null(result.Dependent); + Assert.Null(result.AttributeMember); + } + + [Fact] + public void DeserializeRelationships_PopulatedOneToOneDependent_NavigationPropertyIsPopulated() + { + // Arrange + var content = CreateDocumentWithRelationships("one-to-one-principals", "dependent", "one-to-one-dependents"); + var body = JsonConvert.SerializeObject(content); + + // Act + var result = (OneToOnePrincipal)_deserializer.Deserialize(body); + + // Assert + Assert.Equal(1, result.Id); + Assert.Equal(10, result.Dependent.Id); + Assert.Null(result.AttributeMember); + } + + [Fact] + public void DeserializeRelationships_EmptyOneToOnePrincipal_NavigationPropertyAndForeignKeyAreNull() + { + // Arrange + var content = CreateDocumentWithRelationships("one-to-one-dependents", "principal"); + var body = JsonConvert.SerializeObject(content); + + // Act + var result = (OneToOneDependent)_deserializer.Deserialize(body); + + // Assert + Assert.Equal(1, result.Id); + Assert.Null(result.Principal); + Assert.Null(result.PrincipalId); + } + + [Fact] + public void DeserializeRelationships_EmptyRequiredOneToOnePrincipal_ThrowsFormatException() + { + // Arrange + var content = CreateDocumentWithRelationships("one-to-one-required-dependents", "principal"); + var body = JsonConvert.SerializeObject(content); + + // Act, assert + Assert.Throws(() => _deserializer.Deserialize(body)); + } + + [Fact] + public void DeserializeRelationships_PopulatedOneToOnePrincipal_NavigationPropertyAndForeignKeyArePopulated() + { + // Arrange + var content = CreateDocumentWithRelationships("one-to-one-dependents", "principal", "one-to-one-principals"); + var body = JsonConvert.SerializeObject(content); + + // Act + var result = (OneToOneDependent)_deserializer.Deserialize(body); + + // Assert + Assert.Equal(1, result.Id); + Assert.NotNull(result.Principal); + Assert.Equal(10, result.Principal.Id); + Assert.Equal(10, result.PrincipalId); + Assert.Null(result.AttributeMember); + } + + [Fact] + public void DeserializeRelationships_EmptyOneToManyPrincipal_NavigationAndForeignKeyAreNull() + { + // Arrange + var content = CreateDocumentWithRelationships("one-to-many-dependents", "principal"); + var body = JsonConvert.SerializeObject(content); + + // Act + var result = (OneToManyDependent)_deserializer.Deserialize(body); + + // Assert + Assert.Equal(1, result.Id); + Assert.Null(result.Principal); + Assert.Null(result.PrincipalId); + Assert.Null(result.AttributeMember); + } + + [Fact] + public void DeserializeRelationships_EmptyOneToManyRequiredPrincipal_ThrowsFormatException() + { + // Arrange + var content = CreateDocumentWithRelationships("one-to-many-required-dependents", "principal"); + var body = JsonConvert.SerializeObject(content); + + // Act, assert + Assert.Throws(() => _deserializer.Deserialize(body)); + } + + [Fact] + public void DeserializeRelationships_PopulatedOneToManyPrincipal_NavigationAndForeignKeyArePopulated() + { + // Arrange + var content = CreateDocumentWithRelationships("one-to-many-dependents", "principal", "one-to-many-principals"); + var body = JsonConvert.SerializeObject(content); + + // Act + var result = (OneToManyDependent)_deserializer.Deserialize(body); + + // Assert + Assert.Equal(1, result.Id); + Assert.NotNull(result.Principal); + Assert.Equal(10, result.Principal.Id); + Assert.Equal(10, result.PrincipalId); + Assert.Null(result.AttributeMember); + } + + [Fact] + public void DeserializeRelationships_EmptyOneToManyDependent_NavigationIsNull() + { + // Arrange + var content = CreateDocumentWithRelationships("one-to-many-principals", "dependents"); + var body = JsonConvert.SerializeObject(content); + + // Act + var result = (OneToManyPrincipal)_deserializer.Deserialize(body); + + // Assert + Assert.Equal(1, result.Id); + Assert.Null(result.Dependents); + Assert.Null(result.AttributeMember); + } + + [Fact] + public void DeserializeRelationships_PopulatedOneToManyDependent_NavigationIsPopulated() + { + // Arrange + var content = CreateDocumentWithRelationships("one-to-many-principals", "dependents", "one-to-many-dependents", isToManyData: true); + var body = JsonConvert.SerializeObject(content); + + // Act + var result = (OneToManyPrincipal)_deserializer.Deserialize(body); + + // Assert + Assert.Equal(1, result.Id); + Assert.Single(result.Dependents); + Assert.Equal(10, result.Dependents.First().Id); + Assert.Null(result.AttributeMember); + } + } +} diff --git a/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs new file mode 100644 index 0000000000..de04a47a97 --- /dev/null +++ b/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization; +using Xunit; +using UnitTests.TestModels; +using Person = UnitTests.TestModels.Person; + +namespace UnitTests.Serialization.Serializer +{ + public class ResourceObjectBuilderTests : SerializerTestsSetup + { + private readonly ResourceObjectBuilder _builder; + + public ResourceObjectBuilderTests() + { + _builder = new ResourceObjectBuilder(_resourceGraph, new ResourceObjectBuilderSettings()); + } + + [Fact] + public void EntityToResourceObject_EmptyResource_CanBuild() + { + // Arrange + var entity = new TestResource(); + + // Act + var resourceObject = _builder.Build(entity); + + // Assert + Assert.Null(resourceObject.Attributes); + Assert.Null(resourceObject.Relationships); + Assert.Null(resourceObject.Id); + Assert.Equal("test-resource", resourceObject.Type); + } + + [Fact] + public void EntityToResourceObject_ResourceWithId_CanBuild() + { + // Arrange + var entity = new TestResource() { Id = 1 }; + + // Act + var resourceObject = _builder.Build(entity); + + // Assert + Assert.Equal("1", resourceObject.Id); + Assert.Null(resourceObject.Attributes); + Assert.Null(resourceObject.Relationships); + Assert.Equal("test-resource", resourceObject.Type); + } + + [Theory] + [InlineData(null, null)] + [InlineData("string field", 1)] + public void EntityToResourceObject_ResourceWithIncludedAttrs_CanBuild(string stringFieldValue, int? intFieldValue) + { + // Arrange + var entity = new TestResource() { StringField = stringFieldValue, NullableIntField = intFieldValue }; + var attrs = _resourceGraph.GetAttributes(tr => new { tr.StringField, tr.NullableIntField }); + + // Act + var resourceObject = _builder.Build(entity, attrs); + + // Assert + Assert.NotNull(resourceObject.Attributes); + Assert.Equal(2, resourceObject.Attributes.Keys.Count); + Assert.Equal(stringFieldValue, resourceObject.Attributes["string-field"]); + Assert.Equal(intFieldValue, resourceObject.Attributes["nullable-int-field"]); + } + + [Fact] + public void EntityWithRelationshipsToResourceObject_EmptyResource_CanBuild() + { + // Arrange + var entity = new MultipleRelationshipsPrincipalPart(); + + // Act + var resourceObject = _builder.Build(entity); + + // Assert + Assert.Null(resourceObject.Attributes); + Assert.Null(resourceObject.Relationships); + Assert.Null(resourceObject.Id); + Assert.Equal("multi-principals", resourceObject.Type); + } + + [Fact] + public void EntityWithRelationshipsToResourceObject_ResourceWithId_CanBuild() + { + // Arrange + var entity = new MultipleRelationshipsPrincipalPart + { + PopulatedToOne = new OneToOneDependent { Id = 10 }, + }; + + // Act + var resourceObject = _builder.Build(entity); + + // Assert + Assert.Null(resourceObject.Attributes); + Assert.Null(resourceObject.Relationships); + Assert.Null(resourceObject.Id); + Assert.Equal("multi-principals", resourceObject.Type); + } + + [Fact] + public void EntityWithRelationshipsToResourceObject_WithIncludedRelationshipsAttributes_CanBuild() + { + // Arrange + var entity = new MultipleRelationshipsPrincipalPart + { + PopulatedToOne = new OneToOneDependent { Id = 10 }, + PopulatedToManies = new List { new OneToManyDependent { Id = 20 } } + }; + var relationships = _resourceGraph.GetRelationships(tr => new { tr.PopulatedToManies, tr.PopulatedToOne, tr.EmptyToOne, tr.EmptyToManies }); + + // Act + var resourceObject = _builder.Build(entity, relationships: relationships); + + // Assert + Assert.Equal(4, resourceObject.Relationships.Count); + Assert.Null(resourceObject.Relationships["empty-to-one"].Data); + Assert.Empty((IList)resourceObject.Relationships["empty-to-manies"].Data); + var populatedToOneData = (ResourceIdentifierObject)resourceObject.Relationships["populated-to-one"].Data; + Assert.NotNull(populatedToOneData); + Assert.Equal("10", populatedToOneData.Id); + Assert.Equal("one-to-one-dependents", populatedToOneData.Type); + var populatedToManiesData = (List)resourceObject.Relationships["populated-to-manies"].Data; + Assert.Single(populatedToManiesData); + Assert.Equal("20", populatedToManiesData.First().Id); + Assert.Equal("one-to-many-dependents", populatedToManiesData.First().Type); + } + + [Fact] + public void EntityWithRelationshipsToResourceObject_DeviatingForeignKeyWhileRelationshipIncluded_IgnoresForeignKeyDuringBuild() + { + // Arrange + var entity = new OneToOneDependent { Principal = new OneToOnePrincipal { Id = 10 }, PrincipalId = 123 }; + var relationships = _resourceGraph.GetRelationships(tr => tr.Principal); + + // Act + var resourceObject = _builder.Build(entity, relationships: relationships); + + // Assert + Assert.Single(resourceObject.Relationships); + Assert.NotNull(resourceObject.Relationships["principal"].Data); + var ro = (ResourceIdentifierObject)resourceObject.Relationships["principal"].Data; + Assert.Equal("10", ro.Id); + } + + [Fact] + public void EntityWithRelationshipsToResourceObject_DeviatingForeignKeyAndNoNavigationWhileRelationshipIncluded_IgnoresForeignKeyDuringBuild() + { + // Arrange + var entity = new OneToOneDependent { Principal = null, PrincipalId = 123 }; + var relationships = _resourceGraph.GetRelationships(tr => tr.Principal); + + // Act + var resourceObject = _builder.Build(entity, relationships: relationships); + + // Assert + Assert.Null(resourceObject.Relationships["principal"].Data); + } + + [Fact] + public void EntityWithRequiredRelationshipsToResourceObject_DeviatingForeignKeyWhileRelationshipIncluded_IgnoresForeignKeyDuringBuild() + { + // Arrange + var entity = new OneToOneRequiredDependent { Principal = new OneToOnePrincipal { Id = 10 }, PrincipalId = 123 }; + var relationships = _resourceGraph.GetRelationships(tr => tr.Principal); + + // Act + var resourceObject = _builder.Build(entity, relationships: relationships); + + // Assert + Assert.Single(resourceObject.Relationships); + Assert.NotNull(resourceObject.Relationships["principal"].Data); + var ro = (ResourceIdentifierObject)resourceObject.Relationships["principal"].Data; + Assert.Equal("10", ro.Id); + } + + [Fact] + public void EntityWithRequiredRelationshipsToResourceObject_DeviatingForeignKeyAndNoNavigationWhileRelationshipIncluded_ThrowsNotSupportedException() + { + // Arrange + var entity = new OneToOneRequiredDependent { Principal = null, PrincipalId = 123 }; + var relationships = _resourceGraph.GetRelationships(tr => tr.Principal); + + // Act & assert + Assert.ThrowsAny(() => _builder.Build(entity, relationships: relationships)); + } + + [Fact] + public void EntityWithRequiredRelationshipsToResourceObject_EmptyResourceWhileRelationshipIncluded_ThrowsNotSupportedException() + { + // Arrange + var entity = new OneToOneRequiredDependent(); + var relationships = _resourceGraph.GetRelationships(tr => tr.Principal); + + // Act & assert + Assert.ThrowsAny(() => _builder.Build(entity, relationships: relationships)); + } + } +} diff --git a/test/UnitTests/Serialization/DasherizedResolverTests.cs b/test/UnitTests/Serialization/DasherizedResolverTests.cs deleted file mode 100644 index 5c0c4d08f3..0000000000 --- a/test/UnitTests/Serialization/DasherizedResolverTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -using JsonApiDotNetCore.Serialization; -using Newtonsoft.Json; -using Xunit; - -namespace UnitTests.Serialization -{ - public class DasherizedResolverTests - { - [Fact] - public void Resolver_Dasherizes_Property_Names() - { - // arrange - var obj = new - { - myProp = "val" - }; - - // act - var result = JsonConvert.SerializeObject(obj, - Formatting.None, - new JsonSerializerSettings { ContractResolver = new DasherizedResolver() } - ); - - // assert - Assert.Equal("{\"my-prop\":\"val\"}", result); - } - } -} diff --git a/test/UnitTests/Serialization/DeserializerTestsSetup.cs b/test/UnitTests/Serialization/DeserializerTestsSetup.cs new file mode 100644 index 0000000000..dc9dff4aae --- /dev/null +++ b/test/UnitTests/Serialization/DeserializerTestsSetup.cs @@ -0,0 +1,79 @@ +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization; +using System.Collections.Generic; + +namespace UnitTests.Serialization +{ + public class DeserializerTestsSetup : SerializationTestsSetupBase + { + protected class TestDocumentParser : BaseDocumentParser + { + public TestDocumentParser(IResourceGraph resourceGraph) : base(resourceGraph) { } + + public new object Deserialize(string body) + { + return base.Deserialize(body); + } + + protected override void AfterProcessField(IIdentifiable entity, IResourceField field, RelationshipEntry data = null) { } + } + + protected Document CreateDocumentWithRelationships(string mainType, string relationshipMemberName, string relatedType = null, bool isToManyData = false) + { + var content = CreateDocumentWithRelationships(mainType); + content.SingleData.Relationships.Add(relationshipMemberName, CreateRelationshipData(relatedType, isToManyData)); + return content; + } + + protected Document CreateDocumentWithRelationships(string mainType) + { + return new Document + { + Data = new ResourceObject + { + Id = "1", + Type = mainType, + Relationships = new Dictionary { } + } + }; + } + + protected RelationshipEntry CreateRelationshipData(string relatedType = null, bool isToManyData = false) + { + var data = new RelationshipEntry(); + var rio = relatedType == null ? null : new ResourceIdentifierObject { Id = "10", Type = relatedType }; + + if (isToManyData) + { + data.Data = new List(); + if (relatedType != null) ((List)data.Data).Add(rio); + } + else + { + data.Data = rio; + } + return data; + } + + protected Document CreateTestResourceDocument() + { + return new Document + { + Data = new ResourceObject + { + Type = "test-resource", + Id = "1", + Attributes = new Dictionary + { + { "string-field", "some string" }, + { "int-field", 1 }, + { "nullable-int-field", null }, + { "guid-field", "1a68be43-cc84-4924-a421-7f4d614b7781" }, + { "date-time-field", "9/11/2019 11:41:40 AM" } + } + } + }; + } + } +} diff --git a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs deleted file mode 100644 index da656a9bbd..0000000000 --- a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs +++ /dev/null @@ -1,770 +0,0 @@ -using System; -using System.Collections.Generic; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Request; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Services; -using Moq; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; -using Xunit; - -namespace UnitTests.Serialization -{ - public class JsonApiDeSerializerTests - { - [Fact] - public void Can_Deserialize_Complex_Types() - { - // arrange - var resourceGraphBuilder = new ResourceGraphBuilder(); - resourceGraphBuilder.AddResource("test-resource"); - var resourceGraph = resourceGraphBuilder.Build(); - - var jsonApiContextMock = new Mock(); - jsonApiContextMock.SetupAllProperties(); - jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); - - var jsonApiOptions = new JsonApiOptions(); - jsonApiOptions.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); - jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - - var content = new Document - { - Data = new ResourceObject { - Type = "test-resource", - Id = "1", - Attributes = new Dictionary - { - { "complex-member", new { compoundName = "testName" } } - } - } - }; - - // act - var result = deserializer.Deserialize(JsonConvert.SerializeObject(content)); - - // assert - Assert.NotNull(result.ComplexMember); - Assert.Equal("testName", result.ComplexMember.CompoundName); - } - - [Fact] - public void Can_Deserialize_Complex_List_Types() - { - // arrange - var resourceGraphBuilder = new ResourceGraphBuilder(); - resourceGraphBuilder.AddResource("test-resource"); - var resourceGraph = resourceGraphBuilder.Build(); - - var jsonApiContextMock = new Mock(); - jsonApiContextMock.SetupAllProperties(); - jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); - var jsonApiOptions = new JsonApiOptions(); - jsonApiOptions.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); - jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - - var content = new Document - { - Data = new ResourceObject { - Type = "test-resource", - Id = "1", - Attributes = new Dictionary - { - { "complex-members", new [] { new { compoundName = "testName" } } } - } - } - }; - - // act - var result = deserializer.Deserialize(JsonConvert.SerializeObject(content)); - - // assert - Assert.NotNull(result.ComplexMembers); - Assert.NotEmpty(result.ComplexMembers); - Assert.Equal("testName", result.ComplexMembers[0].CompoundName); - } - - [Fact] - public void Can_Deserialize_Complex_Types_With_Dasherized_Attrs() - { - // arrange - var resourceGraphBuilder = new ResourceGraphBuilder(); - resourceGraphBuilder.AddResource("test-resource"); - var resourceGraph = resourceGraphBuilder.Build(); - - var jsonApiContextMock = new Mock(); - jsonApiContextMock.SetupAllProperties(); - jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); - - var jsonApiOptions = new JsonApiOptions(); - jsonApiOptions.SerializerSettings.ContractResolver = new DasherizedResolver(); // <-- - jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - - var content = new Document - { - Data = new ResourceObject { - Type = "test-resource", - Id = "1", - Attributes = new Dictionary - { - { - "complex-member", new Dictionary { { "compound-name", "testName" } } - } - } - } - }; - - // act - var result = deserializer.Deserialize(JsonConvert.SerializeObject(content)); - - // assert - Assert.NotNull(result.ComplexMember); - Assert.Equal("testName", result.ComplexMember.CompoundName); - } - - [Fact] - public void Immutable_Attrs_Are_Not_Included_In_AttributesToUpdate() - { - // arrange - var resourceGraphBuilder = new ResourceGraphBuilder(); - resourceGraphBuilder.AddResource("test-resource"); - var resourceGraph = resourceGraphBuilder.Build(); - - var attributesToUpdate = new Dictionary(); - - var jsonApiContextMock = new Mock(); - jsonApiContextMock.SetupAllProperties(); - jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(attributesToUpdate); - - var jsonApiOptions = new JsonApiOptions(); - jsonApiOptions.SerializerSettings.ContractResolver = new DasherizedResolver(); - jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - - var content = new Document - { - Data = new ResourceObject { - Type = "test-resource", - Id = "1", - Attributes = new Dictionary - { - { - "complex-member", new Dictionary { { "compound-name", "testName" } } - }, - { "immutable", "value" } - } - } - }; - - var contentString = JsonConvert.SerializeObject(content); - - // act - var result = deserializer.Deserialize(contentString); - - // assert - Assert.NotNull(result.ComplexMember); - Assert.Single(attributesToUpdate); - - foreach (var attr in attributesToUpdate) - Assert.False(attr.Key.IsImmutable); - } - - [Fact] - public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship() - { - // arrange - var resourceGraphBuilder = new ResourceGraphBuilder(); - resourceGraphBuilder.AddResource("independents"); - resourceGraphBuilder.AddResource("dependents"); - var resourceGraph = resourceGraphBuilder.Build(); - - var jsonApiContextMock = new Mock(); - jsonApiContextMock.SetupAllProperties(); - jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); - - var jsonApiOptions = new JsonApiOptions(); - jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - - var property = Guid.NewGuid().ToString(); - var content = new Document - { - Data = new ResourceObject { - Type = "independents", - Id = "1", - Attributes = new Dictionary { { "property", property } } - } - }; - - var contentString = JsonConvert.SerializeObject(content); - - // act - var result = deserializer.Deserialize(contentString); - - // assert - Assert.NotNull(result); - Assert.Equal(property, result.Property); - } - - [Fact] - public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_String_Keys() - { - // arrange - var resourceGraphBuilder = new ResourceGraphBuilder(); - resourceGraphBuilder.AddResource("independents"); - resourceGraphBuilder.AddResource("dependents"); - var resourceGraph = resourceGraphBuilder.Build(); - - var jsonApiContextMock = new Mock(); - jsonApiContextMock.SetupAllProperties(); - jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); - jsonApiContextMock.Setup(m => m.HasOneRelationshipPointers).Returns(new HasOneRelationshipPointers()); - - var jsonApiOptions = new JsonApiOptions(); - jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - - var property = Guid.NewGuid().ToString(); - var content = new Document - { - Data = new ResourceObject - { - Type = "independents", - Id = "natural-key", - Attributes = new Dictionary { { "property", property } }, - Relationships = new Dictionary - { - { "dependent" , new RelationshipData { } } - } - } - }; - - var contentString = JsonConvert.SerializeObject(content); - - // act - var result = deserializer.Deserialize(contentString); - - // assert - Assert.NotNull(result); - Assert.Equal(property, result.Property); - } - - [Fact] - public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_Relationship_Body() - { - // arrange - var resourceGraphBuilder = new ResourceGraphBuilder(); - resourceGraphBuilder.AddResource("independents"); - resourceGraphBuilder.AddResource("dependents"); - var resourceGraph = resourceGraphBuilder.Build(); - - var jsonApiContextMock = new Mock(); - jsonApiContextMock.SetupAllProperties(); - jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); - jsonApiContextMock.Setup(m => m.HasOneRelationshipPointers).Returns(new HasOneRelationshipPointers()); - - var jsonApiOptions = new JsonApiOptions(); - jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - - var property = Guid.NewGuid().ToString(); - var content = new Document - { - Data = new ResourceObject { - Type = "independents", - Id = "1", - Attributes = new Dictionary { { "property", property } }, - // a common case for this is deserialization in unit tests - Relationships = new Dictionary { - { - "dependent", new RelationshipData - { - SingleData = new ResourceIdentifierObject("dependents", "1") - } - } - } - } - }; - - var contentString = JsonConvert.SerializeObject(content); - - // act - var result = deserializer.Deserialize(contentString); - - // assert - Assert.NotNull(result); - Assert.Equal(property, result.Property); - Assert.NotNull(result.Dependent); - Assert.Equal(1, result.Dependent.Id); - } - - [Fact] - public void Sets_The_DocumentMeta_Property_In_JsonApiContext() - { - // arrange - var resourceGraphBuilder = new ResourceGraphBuilder(); - resourceGraphBuilder.AddResource("independents"); - resourceGraphBuilder.AddResource("dependents"); - var resourceGraph = resourceGraphBuilder.Build(); - - var jsonApiContextMock = new Mock(); - jsonApiContextMock.SetupAllProperties(); - jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); - - var jsonApiOptions = new JsonApiOptions(); - jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - - - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - - var property = Guid.NewGuid().ToString(); - - var content = new Document - { - Meta = new Dictionary() { { "foo", "bar" } }, - Data = new ResourceObject { - Type = "independents", - Id = "1", - Attributes = new Dictionary { { "property", property } }, - // a common case for this is deserialization in unit tests - Relationships = new Dictionary { { "dependent", new RelationshipData { } } } - } - }; - - var contentString = JsonConvert.SerializeObject(content); - - // act - var result = deserializer.Deserialize(contentString); - - // assert - jsonApiContextMock.VerifySet(mock => mock.DocumentMeta = content.Meta); - } - - private class TestResource : Identifiable - { - [Attr("complex-member")] - public ComplexType ComplexMember { get; set; } - - [Attr("immutable", isImmutable: true)] - public string Immutable { get; set; } - } - - private class TestResourceWithList : Identifiable - { - [Attr("complex-members")] - public List ComplexMembers { get; set; } - } - - private class ComplexType - { - public string CompoundName { get; set; } - } - - private class Independent : Identifiable - { - [Attr("property")] public string Property { get; set; } - [HasOne("dependent")] public Dependent Dependent { get; set; } - } - - private class Dependent : Identifiable - { - [HasOne("independent")] public Independent Independent { get; set; } - public int IndependentId { get; set; } - } - - private class IndependentWithStringKey : Identifiable - { - [Attr("property")] public string Property { get; set; } - [HasOne("dependent")] public Dependent Dependent { get; set; } - public string DependentId { get; set; } - } - - private class DependentWithStringKey : Identifiable - { - [HasOne("independent")] public Independent Independent { get; set; } - public string IndependentId { get; set; } - } - - [Fact] - public void Can_Deserialize_Object_With_HasManyRelationship() - { - // arrange - var resourceGraphBuilder = new ResourceGraphBuilder(); - resourceGraphBuilder.AddResource("independents"); - resourceGraphBuilder.AddResource("dependents"); - var resourceGraph = resourceGraphBuilder.Build(); - - var jsonApiContextMock = new Mock(); - jsonApiContextMock.SetupAllProperties(); - jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); - jsonApiContextMock.Setup(m => m.RelationshipsToUpdate).Returns(new Dictionary()); - jsonApiContextMock.Setup(m => m.HasManyRelationshipPointers).Returns(new HasManyRelationshipPointers()); - - var jsonApiOptions = new JsonApiOptions(); - jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - - var contentString = - @"{ - ""data"": { - ""type"": ""independents"", - ""id"": ""1"", - ""attributes"": { }, - ""relationships"": { - ""dependents"": { - ""data"": [ - { - ""type"": ""dependents"", - ""id"": ""2"" - } - ] - } - } - } - }"; - - // act - var result = deserializer.Deserialize(contentString); - - // assert - Assert.NotNull(result); - Assert.Equal(1, result.Id); - Assert.NotNull(result.Dependents); - Assert.NotEmpty(result.Dependents); - Assert.Single(result.Dependents); - - var dependent = result.Dependents[0]; - Assert.Equal(2, dependent.Id); - } - - [Fact] - public void Sets_Attribute_Values_On_Included_HasMany_Relationships() - { - // arrange - var resourceGraphBuilder = new ResourceGraphBuilder(); - resourceGraphBuilder.AddResource("independents"); - resourceGraphBuilder.AddResource("dependents"); - var resourceGraph = resourceGraphBuilder.Build(); - - var jsonApiContextMock = new Mock(); - jsonApiContextMock.SetupAllProperties(); - jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); - jsonApiContextMock.Setup(m => m.RelationshipsToUpdate).Returns(new Dictionary()); - jsonApiContextMock.Setup(m => m.HasManyRelationshipPointers).Returns(new HasManyRelationshipPointers()); - - var jsonApiOptions = new JsonApiOptions(); - jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - - var expectedName = "John Doe"; - var contentString = - @"{ - ""data"": { - ""type"": ""independents"", - ""id"": ""1"", - ""attributes"": { }, - ""relationships"": { - ""dependents"": { - ""data"": [ - { - ""type"": ""dependents"", - ""id"": ""2"" - } - ] - } - } - }, - ""included"": [ - { - ""type"": ""dependents"", - ""id"": ""2"", - ""attributes"": { - ""name"": """ + expectedName + @""" - } - } - ] - }"; - - // act - var result = deserializer.Deserialize(contentString); - - // assert - Assert.NotNull(result); - Assert.Equal(1, result.Id); - Assert.NotNull(result.Dependents); - Assert.NotEmpty(result.Dependents); - Assert.Single(result.Dependents); - - var dependent = result.Dependents[0]; - Assert.Equal(2, dependent.Id); - Assert.Equal(expectedName, dependent.Name); - } - - [Fact] - public void Sets_Attribute_Values_On_Included_HasOne_Relationships() - { - // arrange - var resourceGraphBuilder = new ResourceGraphBuilder(); - resourceGraphBuilder.AddResource("independents"); - resourceGraphBuilder.AddResource("dependents"); - var resourceGraph = resourceGraphBuilder.Build(); - - var jsonApiContextMock = new Mock(); - jsonApiContextMock.SetupAllProperties(); - jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); - jsonApiContextMock.Setup(m => m.RelationshipsToUpdate).Returns(new Dictionary()); - jsonApiContextMock.Setup(m => m.HasManyRelationshipPointers).Returns(new HasManyRelationshipPointers()); - jsonApiContextMock.Setup(m => m.HasOneRelationshipPointers).Returns(new HasOneRelationshipPointers()); - - var jsonApiOptions = new JsonApiOptions(); - jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - - var expectedName = "John Doe"; - var contentString = - @"{ - ""data"": { - ""type"": ""dependents"", - ""id"": ""1"", - ""attributes"": { }, - ""relationships"": { - ""independent"": { - ""data"": { - ""type"": ""independents"", - ""id"": ""2"" - } - } - } - }, - ""included"": [ - { - ""type"": ""independents"", - ""id"": ""2"", - ""attributes"": { - ""name"": """ + expectedName + @""" - } - } - ] - }"; - - // act - var result = deserializer.Deserialize(contentString); - - // assert - Assert.NotNull(result); - Assert.Equal(1, result.Id); - Assert.NotNull(result.Independent); - Assert.Equal(2, result.Independent.Id); - Assert.Equal(expectedName, result.Independent.Name); - } - - - [Fact] - public void Can_Deserialize_Nested_Included_HasMany_Relationships() - { - // arrange - var resourceGraphBuilder = new ResourceGraphBuilder(); - resourceGraphBuilder.AddResource("independents"); - resourceGraphBuilder.AddResource("dependents"); - resourceGraphBuilder.AddResource("many-to-manys"); - - var deserializer = GetDeserializer(resourceGraphBuilder); - - var contentString = - @"{ - ""data"": { - ""type"": ""independents"", - ""id"": ""1"", - ""attributes"": { }, - ""relationships"": { - ""many-to-manys"": { - ""data"": [{ - ""type"": ""many-to-manys"", - ""id"": ""2"" - }, { - ""type"": ""many-to-manys"", - ""id"": ""3"" - }] - } - } - }, - ""included"": [ - { - ""type"": ""many-to-manys"", - ""id"": ""2"", - ""attributes"": {}, - ""relationships"": { - ""dependent"": { - ""data"": { - ""type"": ""dependents"", - ""id"": ""4"" - } - }, - ""independent"": { - ""data"": { - ""type"": ""independents"", - ""id"": ""5"" - } - } - } - }, - { - ""type"": ""many-to-manys"", - ""id"": ""3"", - ""attributes"": {}, - ""relationships"": { - ""dependent"": { - ""data"": { - ""type"": ""dependents"", - ""id"": ""4"" - } - }, - ""independent"": { - ""data"": { - ""type"": ""independents"", - ""id"": ""6"" - } - } - } - }, - { - ""type"": ""dependents"", - ""id"": ""4"", - ""attributes"": {}, - ""relationships"": { - ""many-to-manys"": { - ""data"": [{ - ""type"": ""many-to-manys"", - ""id"": ""2"" - }, { - ""type"": ""many-to-manys"", - ""id"": ""3"" - }] - } - } - } - , - { - ""type"": ""independents"", - ""id"": ""5"", - ""attributes"": {}, - ""relationships"": { - ""many-to-manys"": { - ""data"": [{ - ""type"": ""many-to-manys"", - ""id"": ""2"" - }] - } - } - } - , - { - ""type"": ""independents"", - ""id"": ""6"", - ""attributes"": {}, - ""relationships"": { - ""many-to-manys"": { - ""data"": [{ - ""type"": ""many-to-manys"", - ""id"": ""3"" - }] - } - } - } - ] - }"; - - // act - var result = deserializer.Deserialize(contentString); - - // assert - Assert.NotNull(result); - Assert.Equal(1, result.Id); - Assert.NotNull(result.ManyToManys); - Assert.Equal(2, result.ManyToManys.Count); - - // TODO: not sure if this should be a thing that works? - // could this cause cycles in the graph? - // Assert.NotNull(result.ManyToManys[0].Dependent); - // Assert.NotNull(result.ManyToManys[0].Independent); - // Assert.NotNull(result.ManyToManys[1].Dependent); - // Assert.NotNull(result.ManyToManys[1].Independent); - - // Assert.Equal(result.ManyToManys[0].Dependent, result.ManyToManys[1].Dependent); - // Assert.NotEqual(result.ManyToManys[0].Independent, result.ManyToManys[1].Independent); - } - - private JsonApiDeSerializer GetDeserializer(ResourceGraphBuilder resourceGraphBuilder) - { - var resourceGraph = resourceGraphBuilder.Build(); - - var jsonApiContextMock = new Mock(); - jsonApiContextMock.SetupAllProperties(); - jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); - jsonApiContextMock.Setup(m => m.RelationshipsToUpdate).Returns(new Dictionary()); - jsonApiContextMock.Setup(m => m.HasManyRelationshipPointers).Returns(new HasManyRelationshipPointers()); - jsonApiContextMock.Setup(m => m.HasOneRelationshipPointers).Returns(new HasOneRelationshipPointers()); - - var jsonApiOptions = new JsonApiOptions(); - jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - - var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); - - return deserializer; - } - - private class ManyToManyNested : Identifiable - { - [Attr("name")] public string Name { get; set; } - [HasOne("dependent")] public OneToManyDependent Dependent { get; set; } - public int DependentId { get; set; } - [HasOne("independent")] public OneToManyIndependent Independent { get; set; } - public int InependentId { get; set; } - } - - private class OneToManyDependent : Identifiable - { - [Attr("name")] public string Name { get; set; } - [HasOne("independent")] public OneToManyIndependent Independent { get; set; } - public int IndependentId { get; set; } - - [HasMany("many-to-manys")] public List ManyToManys { get; set; } - } - - private class OneToManyIndependent : Identifiable - { - [Attr("name")] public string Name { get; set; } - [HasMany("dependents")] public List Dependents { get; set; } - - [HasMany("many-to-manys")] public List ManyToManys { get; set; } - } - } -} diff --git a/test/UnitTests/Serialization/JsonApiSerializerTests.cs b/test/UnitTests/Serialization/JsonApiSerializerTests.cs deleted file mode 100644 index c781617d03..0000000000 --- a/test/UnitTests/Serialization/JsonApiSerializerTests.cs +++ /dev/null @@ -1,273 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Request; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Services; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using Xunit; - -namespace UnitTests.Serialization -{ - public class JsonApiSerializerTests - { - [Fact] - public void Can_Serialize_Complex_Types() - { - // arrange - var resourceGraphBuilder = new ResourceGraphBuilder(); - resourceGraphBuilder.AddResource("test-resource"); - - var serializer = GetSerializer(resourceGraphBuilder); - - var resource = new TestResource - { - ComplexMember = new ComplexType - { - CompoundName = "testname" - } - }; - - // act - var result = serializer.Serialize(resource); - - // assert - Assert.NotNull(result); - - var expectedFormatted = - @"{ - ""data"": { - ""attributes"": { - ""complex-member"": { - ""compound-name"": ""testname"" - } - }, - ""relationships"": { - ""children"": { - ""links"": { - ""self"": ""/test-resource//relationships/children"", - ""related"": ""/test-resource//children"" - } - } - }, - ""type"": ""test-resource"", - ""id"": """" - } - }"; - var expected = Regex.Replace(expectedFormatted, @"\s+", ""); - - Assert.Equal(expected, result); - } - - [Fact] - public void Can_Serialize_Deeply_Nested_Relationships() - { - // arrange - var resourceGraphBuilder = new ResourceGraphBuilder(); - resourceGraphBuilder.AddResource("test-resource"); - resourceGraphBuilder.AddResource("children"); - resourceGraphBuilder.AddResource("infections"); - - var serializer = GetSerializer( - resourceGraphBuilder, - included: new List { "children.infections" } - ); - - var resource = new TestResource - { - Id = 1, - Children = new List { - new ChildResource { - Id = 2, - Infections = new List { - new InfectionResource { Id = 4 }, - new InfectionResource { Id = 5 }, - } - }, - new ChildResource { - Id = 3 - } - } - }; - - // act - var result = serializer.Serialize(resource); - - // assert - Assert.NotNull(result); - - var expectedFormatted = - @"{ - ""data"": { - ""attributes"": { - ""complex-member"": null - }, - ""relationships"": { - ""children"": { - ""links"": { - ""self"": ""/test-resource/1/relationships/children"", - ""related"": ""/test-resource/1/children"" - }, - ""data"": [{ - ""type"": ""children"", - ""id"": ""2"" - }, { - ""type"": ""children"", - ""id"": ""3"" - }] - } - }, - ""type"": ""test-resource"", - ""id"": ""1"" - }, - ""included"": [ - { - ""attributes"": {}, - ""relationships"": { - ""infections"": { - ""links"": { - ""self"": ""/children/2/relationships/infections"", - ""related"": ""/children/2/infections"" - }, - ""data"": [{ - ""type"": ""infections"", - ""id"": ""4"" - }, { - ""type"": ""infections"", - ""id"": ""5"" - }] - }, - ""parent"": { - ""links"": { - ""self"": ""/children/2/relationships/parent"", - ""related"": ""/children/2/parent"" - } - } - }, - ""type"": ""children"", - ""id"": ""2"" - }, - { - ""attributes"": {}, - ""relationships"": { - ""infected"": { - ""links"": { - ""self"": ""/infections/4/relationships/infected"", - ""related"": ""/infections/4/infected"" - } - } - }, - ""type"": ""infections"", - ""id"": ""4"" - }, - { - ""attributes"": {}, - ""relationships"": { - ""infected"": { - ""links"": { - ""self"": ""/infections/5/relationships/infected"", - ""related"": ""/infections/5/infected"" - } - } - }, - ""type"": ""infections"", - ""id"": ""5"" - }, - { - ""attributes"": {}, - ""relationships"": { - ""infections"": { - ""links"": { - ""self"": ""/children/3/relationships/infections"", - ""related"": ""/children/3/infections"" - } - }, - ""parent"": { - ""links"": { - ""self"": ""/children/3/relationships/parent"", - ""related"": ""/children/3/parent"" - } - } - }, - ""type"": ""children"", - ""id"": ""3"" - } - ] - }"; - var expected = Regex.Replace(expectedFormatted, @"\s+", ""); - - Assert.Equal(expected, result); - } - - private JsonApiSerializer GetSerializer( - ResourceGraphBuilder resourceGraphBuilder, - List included = null) - { - var resourceGraph = resourceGraphBuilder.Build(); - - var jsonApiContextMock = new Mock(); - jsonApiContextMock.SetupAllProperties(); - jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - jsonApiContextMock.Setup(m => m.Options).Returns(new JsonApiOptions()); - jsonApiContextMock.Setup(m => m.RequestEntity).Returns(resourceGraph.GetContextEntity("test-resource")); - // jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); - // jsonApiContextMock.Setup(m => m.RelationshipsToUpdate).Returns(new Dictionary()); - // jsonApiContextMock.Setup(m => m.HasManyRelationshipPointers).Returns(new HasManyRelationshipPointers()); - // jsonApiContextMock.Setup(m => m.HasOneRelationshipPointers).Returns(new HasOneRelationshipPointers()); - jsonApiContextMock.Setup(m => m.MetaBuilder).Returns(new MetaBuilder()); - jsonApiContextMock.Setup(m => m.PageManager).Returns(new PageManager()); - - if (included != null) - jsonApiContextMock.Setup(m => m.IncludedRelationships).Returns(included); - - var jsonApiOptions = new JsonApiOptions(); - jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - - var services = new ServiceCollection(); - - var mvcBuilder = services.AddMvcCore(); - - services - .AddJsonApiInternals(jsonApiOptions); - - var provider = services.BuildServiceProvider(); - var scoped = new TestScopedServiceProvider(provider); - - var documentBuilder = new DocumentBuilder(jsonApiContextMock.Object, scopedServiceProvider: scoped); - var serializer = new JsonApiSerializer(jsonApiContextMock.Object, documentBuilder); - - return serializer; - } - - private class TestResource : Identifiable - { - [Attr("complex-member")] - public ComplexType ComplexMember { get; set; } - - [HasMany("children")] public List Children { get; set; } - } - - private class ComplexType - { - public string CompoundName { get; set; } - } - - private class ChildResource : Identifiable - { - [HasMany("infections")] public List Infections { get; set;} - - [HasOne("parent")] public TestResource Parent { get; set; } - } - - private class InfectionResource : Identifiable - { - [HasOne("infected")] public ChildResource Infected { get; set; } - } - } -} diff --git a/test/UnitTests/Serialization/SerializationTestsSetupBase.cs b/test/UnitTests/Serialization/SerializationTestsSetupBase.cs new file mode 100644 index 0000000000..c56e4a532e --- /dev/null +++ b/test/UnitTests/Serialization/SerializationTestsSetupBase.cs @@ -0,0 +1,64 @@ +using Bogus; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Internal.Contracts; +using UnitTests.TestModels; +using Person = UnitTests.TestModels.Person; + +namespace UnitTests.Serialization +{ + public class SerializationTestsSetupBase + { + protected IResourceGraph _resourceGraph; + protected readonly Faker _foodFaker; + protected readonly Faker _songFaker; + protected readonly Faker
_articleFaker; + protected readonly Faker _blogFaker; + protected readonly Faker _personFaker; + + public SerializationTestsSetupBase() + { + _resourceGraph = BuildGraph(); + _articleFaker = new Faker
() + .RuleFor(f => f.Title, f => f.Hacker.Phrase()) + .RuleFor(f => f.Id, f => f.UniqueIndex + 1); + _personFaker = new Faker() + .RuleFor(f => f.Name, f => f.Person.FullName) + .RuleFor(f => f.Id, f => f.UniqueIndex + 1); + _blogFaker = new Faker() + .RuleFor(f => f.Title, f => f.Hacker.Phrase()) + .RuleFor(f => f.Id, f => f.UniqueIndex + 1); + _songFaker = new Faker() + .RuleFor(f => f.Title, f => f.Lorem.Sentence()) + .RuleFor(f => f.Id, f => f.UniqueIndex + 1); + _foodFaker = new Faker() + .RuleFor(f => f.Dish, f => f.Lorem.Sentence()) + .RuleFor(f => f.Id, f => f.UniqueIndex + 1); + } + + protected IResourceGraph BuildGraph() + { + var resourceGraphBuilder = new ResourceGraphBuilder(); + resourceGraphBuilder.AddResource("test-resource"); + resourceGraphBuilder.AddResource("test-resource-with-list"); + // one to one relationships + resourceGraphBuilder.AddResource("one-to-one-principals"); + resourceGraphBuilder.AddResource("one-to-one-dependents"); + resourceGraphBuilder.AddResource("one-to-one-required-dependents"); + // one to many relationships + resourceGraphBuilder.AddResource("one-to-many-principals"); + resourceGraphBuilder.AddResource("one-to-many-dependents"); + resourceGraphBuilder.AddResource("one-to-many-required-dependents"); + // collective relationships + resourceGraphBuilder.AddResource("multi-principals"); + resourceGraphBuilder.AddResource("multi-dependents"); + + resourceGraphBuilder.AddResource
(); + resourceGraphBuilder.AddResource(); + resourceGraphBuilder.AddResource(); + resourceGraphBuilder.AddResource(); + resourceGraphBuilder.AddResource(); + + return resourceGraphBuilder.Build(); + } + } +} \ No newline at end of file diff --git a/test/UnitTests/Serialization/SerializerTestsSetup.cs b/test/UnitTests/Serialization/SerializerTestsSetup.cs new file mode 100644 index 0000000000..133482a5d1 --- /dev/null +++ b/test/UnitTests/Serialization/SerializerTestsSetup.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.Links; +using JsonApiDotNetCore.Query; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Server; +using JsonApiDotNetCore.Serialization.Server.Builders; +using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreExample.Models; +using Moq; + +namespace UnitTests.Serialization +{ + public class SerializerTestsSetup : SerializationTestsSetupBase + { + protected readonly TopLevelLinks _dummyToplevelLinks; + protected readonly ResourceLinks _dummyResourceLinks; + protected readonly RelationshipLinks _dummyRelationshipLinks; + public SerializerTestsSetup() + { + _dummyToplevelLinks = new TopLevelLinks + { + Self = "http://www.dummy.com/dummy-self-link", + Next = "http://www.dummy.com/dummy-next-link", + Prev = "http://www.dummy.com/dummy-prev-link", + First = "http://www.dummy.com/dummy-first-link", + Last = "http://www.dummy.com/dummy-last-link" + }; + _dummyResourceLinks = new ResourceLinks + { + Self = "http://www.dummy.com/dummy-resource-self-link" + }; + _dummyRelationshipLinks = new RelationshipLinks + { + Related = "http://www.dummy.com/dummy-relationship-related-link", + Self = "http://www.dummy.com/dummy-relationship-self-link" + }; + } + + protected ResponseSerializer GetResponseSerializer(List> inclusionChains = null, Dictionary metaDict = null, TopLevelLinks topLinks = null, ResourceLinks resourceLinks = null, RelationshipLinks relationshipLinks = null) where T : class, IIdentifiable + { + var meta = GetMetaBuilder(metaDict); + var link = GetLinkBuilder(topLinks, resourceLinks, relationshipLinks); + var included = GetIncludedRelationships(inclusionChains); + var includedBuilder = GetIncludedBuilder(); + var fieldsToSerialize = GetSerializableFields(); + var provider = GetResourceContextProvider(); + ResponseResourceObjectBuilder resourceObjectBuilder = new ResponseResourceObjectBuilder(link, includedBuilder, included, _resourceGraph, GetSerializerSettingsProvider()); + return new ResponseSerializer(meta, link, includedBuilder, fieldsToSerialize, resourceObjectBuilder, provider); + } + + protected ResponseResourceObjectBuilder GetResponseResourceObjectBuilder(List> inclusionChains = null, ResourceLinks resourceLinks = null, RelationshipLinks relationshipLinks = null) + { + var link = GetLinkBuilder(null, resourceLinks, relationshipLinks); + var included = GetIncludedRelationships(inclusionChains); + var includedBuilder = GetIncludedBuilder(); + return new ResponseResourceObjectBuilder(link, includedBuilder, included, _resourceGraph, GetSerializerSettingsProvider()); + } + + private IIncludedResourceObjectBuilder GetIncludedBuilder() + { + return new IncludedResourceObjectBuilder(GetSerializableFields(), GetLinkBuilder(), _resourceGraph, GetSerializerSettingsProvider()); + } + + protected IResourceObjectBuilderSettingsProvider GetSerializerSettingsProvider() + { + var mock = new Mock(); + mock.Setup(m => m.Get()).Returns(new ResourceObjectBuilderSettings()); + return mock.Object; + } + + private IResourceGraph GetResourceContextProvider() + { + return _resourceGraph; + } + + protected IMetaBuilder GetMetaBuilder(Dictionary meta = null) where T : class, IIdentifiable + { + var mock = new Mock>(); + mock.Setup(m => m.GetMeta()).Returns(meta); + return mock.Object; + } + + protected ICurrentRequest GetRequestManager() where T : class, IIdentifiable + { + var mock = new Mock(); + mock.Setup(m => m.GetRequestResource()).Returns(_resourceGraph.GetResourceContext()); + return mock.Object; + } + + protected ILinkBuilder GetLinkBuilder(TopLevelLinks top = null, ResourceLinks resource = null, RelationshipLinks relationship = null) + { + var mock = new Mock(); + mock.Setup(m => m.GetTopLevelLinks(It.IsAny())).Returns(top); + mock.Setup(m => m.GetResourceLinks(It.IsAny(), It.IsAny())).Returns(resource); + mock.Setup(m => m.GetRelationshipLinks(It.IsAny(), It.IsAny())).Returns(relationship); + return mock.Object; + } + + protected ISparseFieldsService GetFieldsQuery() + { + var mock = new Mock(); + return mock.Object; + } + + protected IFieldsToSerialize GetSerializableFields() + { + var mock = new Mock(); + mock.Setup(m => m.GetAllowedAttributes(It.IsAny(), It.IsAny())).Returns((t, r) => _resourceGraph.GetResourceContext(t).Attributes); + mock.Setup(m => m.GetAllowedRelationships(It.IsAny())).Returns(t => _resourceGraph.GetResourceContext(t).Relationships); + return mock.Object; + } + + protected IIncludeService GetIncludedRelationships(List> inclusionChains = null) + { + var mock = new Mock(); + if (inclusionChains != null) + mock.Setup(m => m.Get()).Returns(inclusionChains); + + return mock.Object; + } + + /// + /// Minimal implementation of abstract JsonApiSerializer base class, with + /// the purpose of testing the business logic for building the document structure. + /// + protected class TestDocumentBuilder : BaseDocumentBuilder + { + public TestDocumentBuilder(IResourceObjectBuilder resourceObjectBuilder, IResourceContextProvider provider) : base(resourceObjectBuilder, provider) { } + + public new Document Build(IIdentifiable entity, List attributes = null, List relationships = null) + { + return base.Build(entity, attributes ?? null, relationships ?? null); + } + + public new Document Build(IEnumerable entities, List attributes = null, List relationships = null) + { + return base.Build(entities, attributes ?? null, relationships ?? null); + } + } + } +} \ No newline at end of file diff --git a/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs new file mode 100644 index 0000000000..b477023432 --- /dev/null +++ b/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs @@ -0,0 +1,179 @@ +using JsonApiDotNetCore.Models; +using Xunit; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Serialization.Server.Builders; +using UnitTests.TestModels; +using Person = UnitTests.TestModels.Person; + +namespace UnitTests.Serialization.Server +{ + public class IncludedResourceObjectBuilderTests : SerializerTestsSetup + { + [Fact] + public void BuildIncluded_DeeplyNestedCircularChainOfSingleData_CanBuild() + { + // Arrange + var (article, author, authorFood, reviewer, reviewerFood) = GetAuthorChainInstances(); + var authorChain = GetIncludedRelationshipsChain("author.blogs.reviewer.favorite-food"); + var builder = GetBuilder(); + + // Act + builder.IncludeRelationshipChain(authorChain, article); + var result = builder.Build(); + + // Assert + Assert.Equal(6, result.Count); + + var authorResourceObject = result.Single((ro) => ro.Type == "people" && ro.Id == author.StringId); + var authorFoodRelation = authorResourceObject.Relationships["favorite-food"].SingleData; + Assert.Equal(author.FavoriteFood.StringId, authorFoodRelation.Id); + + var reviewerResourceObject = result.Single((ro) => ro.Type == "people" && ro.Id == reviewer.StringId); + var reviewerFoodRelation = reviewerResourceObject.Relationships["favorite-food"].SingleData; + Assert.Equal(reviewer.FavoriteFood.StringId, reviewerFoodRelation.Id); + } + + [Fact] + public void BuildIncluded_DeeplyNestedCircularChainOfManyData_BuildsWithoutDuplicates() + { + // Arrange + var (article, author, _, _, _) = GetAuthorChainInstances(); + var secondArticle = _articleFaker.Generate(); + secondArticle.Author = author; + var builder = GetBuilder(); + + // Act + var authorChain = GetIncludedRelationshipsChain("author.blogs.reviewer.favorite-food"); + builder.IncludeRelationshipChain(authorChain, article); + builder.IncludeRelationshipChain(authorChain, secondArticle); + + // Assert + var result = builder.Build(); + Assert.Equal(6, result.Count); + } + + [Fact] + public void BuildIncluded_OverlappingDeeplyNestedCirculairChains_CanBuild() + { + // Arrange + var authorChain = GetIncludedRelationshipsChain("author.blogs.reviewer.favorite-food"); + var (article, author, authorFood, reviewer, reviewerFood) = GetAuthorChainInstances(); + var sharedBlog = author.Blogs.First(); + var sharedBlogAuthor = reviewer; + var (_reviewer, _reviewerSong, _author, _authorSong) = GetReviewerChainInstances(article, sharedBlog, sharedBlogAuthor); + var reviewerChain = GetIncludedRelationshipsChain("reviewer.blogs.author.favorite-song"); + var builder = GetBuilder(); + + // Act + builder.IncludeRelationshipChain(authorChain, article); + builder.IncludeRelationshipChain(reviewerChain, article); + var result = builder.Build(); + + // Assert + Assert.Equal(10, result.Count); + var overlappingBlogResourcObject = result.Single((ro) => ro.Type == "blogs" && ro.Id == sharedBlog.StringId); + + Assert.Equal(2, overlappingBlogResourcObject.Relationships.Keys.ToList().Count); + var nonOverlappingBlogs = result.Where((ro) => ro.Type == "blogs" && ro.Id != sharedBlog.StringId).ToList(); + + foreach (var blog in nonOverlappingBlogs) + Assert.Single(blog.Relationships.Keys.ToList()); + + var sharedAuthorResourceObject = result.Single((ro) => ro.Type == "people" && ro.Id == sharedBlogAuthor.StringId); + var sharedAuthorSongRelation = sharedAuthorResourceObject.Relationships["favorite-song"].SingleData; + Assert.Equal(_authorSong.StringId, sharedBlogAuthor.FavoriteSong.StringId); + var sharedAuthorFoodRelation = sharedAuthorResourceObject.Relationships["favorite-food"].SingleData; + Assert.Equal(reviewerFood.StringId, sharedBlogAuthor.FavoriteFood.StringId); + } + + private (Person, Song, Person, Song) GetReviewerChainInstances(Article article, Blog sharedBlog, Person sharedBlogAuthor) + { + var reviewer = _personFaker.Generate(); + article.Reviewer = reviewer; + + var blogs = _blogFaker.Generate(1).ToList(); + blogs.Add(sharedBlog); + reviewer.Blogs = blogs; + + blogs[0].Author = reviewer; + var author = _personFaker.Generate(); + blogs[1].Author = sharedBlogAuthor; + + var authorSong = _songFaker.Generate(); + author.FavoriteSong = authorSong; + sharedBlogAuthor.FavoriteSong = authorSong; + + var reviewerSong = _songFaker.Generate(); + reviewer.FavoriteSong = reviewerSong; + + return (reviewer, reviewerSong, author, authorSong); + } + + private (Article, Person, Food, Person, Food) GetAuthorChainInstances() + { + var article = _articleFaker.Generate(); + var author = _personFaker.Generate(); + article.Author = author; + + var blogs = _blogFaker.Generate(2).ToList(); + author.Blogs = blogs; + + blogs[0].Reviewer = author; + var reviewer = _personFaker.Generate(); + blogs[1].Reviewer = reviewer; + + var authorFood = _foodFaker.Generate(); + author.FavoriteFood = authorFood; + var reviewerFood = _foodFaker.Generate(); + reviewer.FavoriteFood = reviewerFood; + + return (article, author, authorFood, reviewer, reviewerFood); + } + + [Fact] + public void BuildIncluded_DuplicateChildrenMultipleChains_OnceInOutput() + { + var person = _personFaker.Generate(); + var articles = _articleFaker.Generate(5).ToList(); + articles.ForEach(a => a.Author = person); + articles.ForEach(a => a.Reviewer = person); + var builder = GetBuilder(); + var authorChain = GetIncludedRelationshipsChain("author"); + var reviewerChain = GetIncludedRelationshipsChain("reviewer"); + foreach (var article in articles) + { + builder.IncludeRelationshipChain(authorChain, article); + builder.IncludeRelationshipChain(reviewerChain, article); + } + + var result = builder.Build(); + Assert.Single(result); + Assert.Equal(person.Name, result[0].Attributes["name"]); + Assert.Equal(person.Id.ToString(), result[0].Id); + } + + private List GetIncludedRelationshipsChain(string chain) + { + var parsedChain = new List(); + var resourceContext = _resourceGraph.GetResourceContext
(); + var splittedPath = chain.Split(QueryConstants.DOT); + foreach (var requestedRelationship in splittedPath) + { + var relationship = resourceContext.Relationships.Single(r => r.PublicRelationshipName == requestedRelationship); + parsedChain.Add(relationship); + resourceContext = _resourceGraph.GetResourceContext(relationship.RightType); + } + return parsedChain; + } + + private IncludedResourceObjectBuilder GetBuilder() + { + var fields = GetSerializableFields(); + var links = GetLinkBuilder(); + return new IncludedResourceObjectBuilder(fields, links, _resourceGraph, GetSerializerSettingsProvider()); + } + + } +} diff --git a/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs b/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs new file mode 100644 index 0000000000..ac68e8b37b --- /dev/null +++ b/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Server; +using Moq; +using Newtonsoft.Json; +using UnitTests.TestModels; +using Person = UnitTests.TestModels.Person; +using Xunit; + + +namespace UnitTests.Serialization.Server +{ + public class RequestDeserializerTests : DeserializerTestsSetup + { + private readonly RequestDeserializer _deserializer; + private readonly Mock _fieldsManagerMock = new Mock(); + public RequestDeserializerTests() : base() + { + _deserializer = new RequestDeserializer(_resourceGraph, _fieldsManagerMock.Object); + } + + [Fact] + public void DeserializeAttributes_VariousUpdatedMembers_RegistersTargetedFields() + { + // Arrange + SetupFieldsManager(out List attributesToUpdate, out List relationshipsToUpdate); + Document content = CreateTestResourceDocument(); + var body = JsonConvert.SerializeObject(content); + + // Act + _deserializer.Deserialize(body); + + // Assert + Assert.Equal(5, attributesToUpdate.Count); + Assert.Empty(relationshipsToUpdate); + } + + [Fact] + public void DeserializeAttributes_UpdatedImmutableMember_ThrowsInvalidOperationException() + { + // Arrange + SetupFieldsManager(out List attributesToUpdate, out List relationshipsToUpdate); + var content = new Document + { + Data = new ResourceObject + { + Type = "test-resource", + Id = "1", + Attributes = new Dictionary + { + { "immutable", "some string" }, + } + } + }; + var body = JsonConvert.SerializeObject(content); + + // Act, assert + Assert.Throws(() => _deserializer.Deserialize(body)); + } + + [Fact] + public void DeserializeRelationships_MultipleDependentRelationships_RegistersUpdatedRelationships() + { + // Arrange + SetupFieldsManager(out List attributesToUpdate, out List relationshipsToUpdate); + var content = CreateDocumentWithRelationships("multi-principals"); + content.SingleData.Relationships.Add("populated-to-one", CreateRelationshipData("one-to-one-dependents")); + content.SingleData.Relationships.Add("empty-to-one", CreateRelationshipData()); + content.SingleData.Relationships.Add("populated-to-manies", CreateRelationshipData("one-to-many-dependents", isToManyData: true)); + content.SingleData.Relationships.Add("empty-to-manies", CreateRelationshipData(isToManyData: true)); + var body = JsonConvert.SerializeObject(content); + + // Act + _deserializer.Deserialize(body); + + // Assert + Assert.Equal(4, relationshipsToUpdate.Count); + Assert.Empty(attributesToUpdate); + } + + [Fact] + public void DeserializeRelationships_MultiplePrincipalRelationships_RegistersUpdatedRelationships() + { + // Arrange + SetupFieldsManager(out List attributesToUpdate, out List relationshipsToUpdate); + var content = CreateDocumentWithRelationships("multi-dependents"); + content.SingleData.Relationships.Add("populated-to-one", CreateRelationshipData("one-to-one-principals")); + content.SingleData.Relationships.Add("empty-to-one", CreateRelationshipData()); + content.SingleData.Relationships.Add("populated-to-many", CreateRelationshipData("one-to-many-principals")); + content.SingleData.Relationships.Add("empty-to-many", CreateRelationshipData()); + var body = JsonConvert.SerializeObject(content); + + // Act + _deserializer.Deserialize(body); + + // Assert + Assert.Equal(4, relationshipsToUpdate.Count); + Assert.Empty(attributesToUpdate); + } + + private void SetupFieldsManager(out List attributesToUpdate, out List relationshipsToUpdate) + { + attributesToUpdate = new List(); + relationshipsToUpdate = new List(); + _fieldsManagerMock.Setup(m => m.Attributes).Returns(attributesToUpdate); + _fieldsManagerMock.Setup(m => m.Relationships).Returns(relationshipsToUpdate); + } + } +} diff --git a/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs new file mode 100644 index 0000000000..c6fa74e7b1 --- /dev/null +++ b/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Models; +using Xunit; +using UnitTests.TestModels; +using Person = UnitTests.TestModels.Person; + +namespace UnitTests.Serialization.Server +{ + public class ResponseResourceObjectBuilderTests : SerializerTestsSetup + { + private readonly List _relationshipsForBuild; + private const string _relationshipName = "dependents"; + + public ResponseResourceObjectBuilderTests() + { + _relationshipsForBuild = _resourceGraph.GetRelationships(e => new { e.Dependents }); + } + + [Fact] + public void Build_RelationshipNotIncludedAndLinksEnabled_RelationshipEntryWithLinks() + { + // Arrange + var entity = new OneToManyPrincipal { Id = 10 }; + var builder = GetResponseResourceObjectBuilder(relationshipLinks: _dummyRelationshipLinks); + + // Act + var resourceObject = builder.Build(entity, relationships: _relationshipsForBuild); + + // Assert + Assert.True(resourceObject.Relationships.TryGetValue(_relationshipName, out var entry)); + Assert.Equal("http://www.dummy.com/dummy-relationship-self-link", entry.Links.Self); + Assert.Equal("http://www.dummy.com/dummy-relationship-related-link", entry.Links.Related); + Assert.False(entry.IsPopulated); + } + + [Fact] + public void Build_RelationshipNotIncludedAndLinksDisabled_NoRelationshipObject() + { + // Arrange + var entity = new OneToManyPrincipal { Id = 10 }; + var builder = GetResponseResourceObjectBuilder(); + + // Act + var resourceObject = builder.Build(entity, relationships: _relationshipsForBuild); + + // Assert + Assert.Null(resourceObject.Relationships); + } + + [Fact] + public void Build_RelationshipIncludedAndLinksDisabled_RelationshipEntryWithData() + { + // Arrange + var entity = new OneToManyPrincipal { Id = 10, Dependents = new List { new OneToManyDependent { Id = 20 } } }; + var builder = GetResponseResourceObjectBuilder(inclusionChains: new List> { _relationshipsForBuild } ); + + // Act + var resourceObject = builder.Build(entity, relationships: _relationshipsForBuild); + + // Assert + Assert.True(resourceObject.Relationships.TryGetValue(_relationshipName, out var entry)); + Assert.Null(entry.Links); + Assert.True(entry.IsPopulated); + Assert.Equal("20", entry.ManyData.Single().Id); + } + + [Fact] + public void Build_RelationshipIncludedAndLinksEnabled_RelationshipEntryWithDataAndLinks() + { + // Arrange + var entity = new OneToManyPrincipal { Id = 10, Dependents = new List { new OneToManyDependent { Id = 20 } } }; + var builder = GetResponseResourceObjectBuilder(inclusionChains: new List> { _relationshipsForBuild }, relationshipLinks: _dummyRelationshipLinks); + + // Act + var resourceObject = builder.Build(entity, relationships: _relationshipsForBuild); + + // Assert + Assert.True(resourceObject.Relationships.TryGetValue(_relationshipName, out var entry)); + Assert.Equal("http://www.dummy.com/dummy-relationship-self-link", entry.Links.Self); + Assert.Equal("http://www.dummy.com/dummy-relationship-related-link", entry.Links.Related); + Assert.True(entry.IsPopulated); + Assert.Equal("20", entry.ManyData.Single().Id); + } + } +} diff --git a/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs b/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs new file mode 100644 index 0000000000..382755f6b5 --- /dev/null +++ b/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs @@ -0,0 +1,489 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; +using Newtonsoft.Json; +using Xunit; +using UnitTests.TestModels; + +namespace UnitTests.Serialization.Server +{ + public class ResponseSerializerTests : SerializerTestsSetup + { + [Fact] + public void SerializeSingle_ResourceWithDefaultTargetFields_CanSerialize() + { + // Arrange + var entity = new TestResource() { Id = 1, StringField = "value", NullableIntField = 123 }; + var serializer = GetResponseSerializer(); + + // Act + string serialized = serializer.SerializeSingle(entity); + + // Assert + var expectedFormatted = + @"{ + ""data"":{ + ""type"":""test-resource"", + ""id"":""1"", + ""attributes"":{ + ""string-field"":""value"", + ""date-time-field"":""0001-01-01T00:00:00"", + ""nullable-date-time-field"":null, + ""int-field"":0, + ""nullable-int-field"":123, + ""guid-field"":""00000000-0000-0000-0000-000000000000"", + ""complex-field"":null, + ""immutable"":null + } + } + }"; + + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeMany_ResourceWithDefaultTargetFields_CanSerialize() + { + // Arrange + var entity = new TestResource() { Id = 1, StringField = "value", NullableIntField = 123 }; + var serializer = GetResponseSerializer(); + + // Act + string serialized = serializer.SerializeMany(new List { entity }); + + // Assert + var expectedFormatted = + @"{ + ""data"":[{ + ""type"":""test-resource"", + ""id"":""1"", + ""attributes"":{ + ""string-field"":""value"", + ""date-time-field"":""0001-01-01T00:00:00"", + ""nullable-date-time-field"":null, + ""int-field"":0, + ""nullable-int-field"":123, + ""guid-field"":""00000000-0000-0000-0000-000000000000"", + ""complex-field"":null, + ""immutable"":null + } + }] + }"; + + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeSingle_ResourceWithIncludedRelationships_CanSerialize() + { + // Arrange + var entity = new MultipleRelationshipsPrincipalPart + { + Id = 1, + PopulatedToOne = new OneToOneDependent { Id = 10 }, + PopulatedToManies = new List { new OneToManyDependent { Id = 20 } } + }; + var chain = _resourceGraph.GetRelationships().Select(r => new List { r }).ToList(); + var serializer = GetResponseSerializer(inclusionChains: chain); + + // Act + string serialized = serializer.SerializeSingle(entity); + + // Assert + var expectedFormatted = + @"{ + ""data"":{ + ""type"":""multi-principals"", + ""id"":""1"", + ""attributes"":{ ""attribute-member"":null }, + ""relationships"":{ + ""populated-to-one"":{ + ""data"":{ + ""type"":""one-to-one-dependents"", + ""id"":""10"" + } + }, + ""empty-to-one"": { ""data"":null }, + ""populated-to-manies"":{ + ""data"":[ + { + ""type"":""one-to-many-dependents"", + ""id"":""20"" + } + ] + }, + ""empty-to-manies"": { ""data"":[ ] }, + ""multi"":{ ""data"":null } + } + }, + ""included"":[ + { + ""type"":""one-to-one-dependents"", + ""id"":""10"", + ""attributes"":{ ""attribute-member"":null } + }, + { + ""type"":""one-to-many-dependents"", + ""id"":""20"", + ""attributes"":{ ""attribute-member"":null } + } + ] + }"; + + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeSingle_ResourceWithDeeplyIncludedRelationships_CanSerialize() + { + // Arrange + var deeplyIncludedEntity = new OneToManyPrincipal { Id = 30, AttributeMember = "deep" }; + var includedEntity = new OneToManyDependent { Id = 20, Principal = deeplyIncludedEntity }; + var entity = new MultipleRelationshipsPrincipalPart + { + Id = 10, + PopulatedToManies = new List { includedEntity } + }; + + var chains = _resourceGraph.GetRelationships() + .Select(r => + { + var chain = new List { r }; + if (r.PublicRelationshipName != "populated-to-manies") + return new List { r }; + chain.AddRange(_resourceGraph.GetRelationships()); + return chain; + }).ToList(); + + var serializer = GetResponseSerializer(inclusionChains: chains); + + // Act + string serialized = serializer.SerializeSingle(entity); + + // Assert + var expectedFormatted = + @"{ + ""data"":{ + ""type"":""multi-principals"", + ""id"":""10"", + ""attributes"":{ + ""attribute-member"":null + }, + ""relationships"":{ + ""populated-to-one"":{ + ""data"":null + }, + ""empty-to-one"":{ + ""data"":null + }, + ""populated-to-manies"":{ + ""data"":[ + { + ""type"":""one-to-many-dependents"", + ""id"":""20"" + } + ] + }, + ""empty-to-manies"":{ + ""data"":[] + }, + ""multi"":{ + ""data"":null + } + } + }, + ""included"":[ + { + ""type"":""one-to-many-dependents"", + ""id"":""20"", + ""attributes"":{ + ""attribute-member"":null + }, + ""relationships"":{ + ""principal"":{ + ""data"":{ + ""type"":""one-to-many-principals"", + ""id"":""30"" + } + } + } + }, + { + ""type"":""one-to-many-principals"", + ""id"":""30"", + ""attributes"":{ + ""attribute-member"":""deep"" + } + } + ] + }"; + + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeSingle_Null_CanSerialize() + { + // Arrange + var serializer = GetResponseSerializer(); + TestResource entity = null; + // Act + string serialized = serializer.SerializeSingle(entity); + + // Assert + var expectedFormatted = @"{ ""data"": null }"; + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeList_EmptyList_CanSerialize() + { + // Arrange + var serializer = GetResponseSerializer(); + // Act + string serialized = serializer.SerializeMany(new List()); + + // Assert + var expectedFormatted = @"{ ""data"": [] }"; + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeSingle_ResourceWithLinksEnabled_CanSerialize() + { + // Arrange + var entity = new OneToManyPrincipal { Id = 10 }; + var serializer = GetResponseSerializer(topLinks: _dummyToplevelLinks, relationshipLinks: _dummyRelationshipLinks, resourceLinks: _dummyResourceLinks); + + // Act + string serialized = serializer.SerializeSingle(entity); + + // Assert + var expectedFormatted = + @"{ + ""links"":{ + ""self"":""http://www.dummy.com/dummy-self-link"", + ""next"":""http://www.dummy.com/dummy-next-link"", + ""prev"":""http://www.dummy.com/dummy-prev-link"", + ""first"":""http://www.dummy.com/dummy-first-link"", + ""last"":""http://www.dummy.com/dummy-last-link"" + }, + ""data"":{ + ""type"":""one-to-many-principals"", + ""id"":""10"", + ""attributes"":{ + ""attribute-member"":null + }, + ""relationships"":{ + ""dependents"":{ + ""links"":{ + ""self"":""http://www.dummy.com/dummy-relationship-self-link"", + ""related"":""http://www.dummy.com/dummy-relationship-related-link"" + } + } + }, + ""links"":{ + ""self"":""http://www.dummy.com/dummy-resource-self-link"" + } + } + }"; + + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeSingle_ResourceWithMeta_IncludesMetaInResult() + { + // Arrange + var meta = new Dictionary { { "test", "meta" } }; + var entity = new OneToManyPrincipal { Id = 10 }; + var serializer = GetResponseSerializer(metaDict: meta); + + // Act + string serialized = serializer.SerializeSingle(entity); + + // Assert + var expectedFormatted = + @"{ + ""meta"":{ ""test"": ""meta"" }, + ""data"":{ + ""type"":""one-to-many-principals"", + ""id"":""10"", + ""attributes"":{ + ""attribute-member"":null + } + } + }"; + + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeSingle_NullWithLinksAndMeta_StillShowsLinksAndMeta() + { + // Arrange + var meta = new Dictionary { { "test", "meta" } }; + OneToManyPrincipal entity = null; + var serializer = GetResponseSerializer(metaDict: meta, topLinks: _dummyToplevelLinks, relationshipLinks: _dummyRelationshipLinks, resourceLinks: _dummyResourceLinks); + // Act + string serialized = serializer.SerializeSingle(entity); + // Assert + var expectedFormatted = + @"{ + ""meta"":{ ""test"": ""meta"" }, + ""links"":{ + ""self"":""http://www.dummy.com/dummy-self-link"", + ""next"":""http://www.dummy.com/dummy-next-link"", + ""prev"":""http://www.dummy.com/dummy-prev-link"", + ""first"":""http://www.dummy.com/dummy-first-link"", + ""last"":""http://www.dummy.com/dummy-last-link"" + }, + ""data"": null + }"; + + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeSingleWithRequestRelationship_NullToOneRelationship_CanSerialize() + { + // Arrange + var entity = new OneToOnePrincipal() { Id = 2, Dependent = null }; + var serializer = GetResponseSerializer(); + var requestRelationship = _resourceGraph.GetRelationships((OneToOnePrincipal t) => t.Dependent).First(); + serializer.RequestRelationship = requestRelationship; + + // Act + string serialized = serializer.SerializeSingle(entity); + + // Assert + var expectedFormatted = @"{ ""data"": null}"; + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeSingleWithRequestRelationship_PopulatedToOneRelationship_CanSerialize() + { + // Arrange + var entity = new OneToOnePrincipal() { Id = 2, Dependent = new OneToOneDependent { Id = 1 } }; + var serializer = GetResponseSerializer(); + var requestRelationship = _resourceGraph.GetRelationships((OneToOnePrincipal t) => t.Dependent).First(); + serializer.RequestRelationship = requestRelationship; + + + // Act + string serialized = serializer.SerializeSingle(entity); + + // Assert + var expectedFormatted = + @"{ + ""data"":{ + ""type"":""one-to-one-dependents"", + ""id"":""1"" + } + }"; + + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeSingleWithRequestRelationship_EmptyToManyRelationship_CanSerialize() + { + // Arrange + var entity = new OneToManyPrincipal() { Id = 2, Dependents = new List() }; + var serializer = GetResponseSerializer(); + var requestRelationship = _resourceGraph.GetRelationships((OneToManyPrincipal t) => t.Dependents).First(); + serializer.RequestRelationship = requestRelationship; + + + // Act + string serialized = serializer.SerializeSingle(entity); + + // Assert + var expectedFormatted = @"{ ""data"": [] }"; + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeSingleWithRequestRelationship_PopulatedToManyRelationship_CanSerialize() + { + // Arrange + var entity = new OneToManyPrincipal() { Id = 2, Dependents = new List { new OneToManyDependent { Id = 1 } } }; + var serializer = GetResponseSerializer(); + var requestRelationship = _resourceGraph.GetRelationships((OneToManyPrincipal t) => t.Dependents).First(); + serializer.RequestRelationship = requestRelationship; + + + // Act + string serialized = serializer.SerializeSingle(entity); + + // Assert + var expectedFormatted = + @"{ + ""data"":[{ + ""type"":""one-to-many-dependents"", + ""id"":""1"" + }] + }"; + + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + + Assert.Equal(expected, serialized); + } + + [Fact] + public void SerializeError_CustomError_CanSerialize() + { + // Arrange + var error = new CustomError(507, "title", "detail", "custom"); + var errorCollection = new ErrorCollection(); + errorCollection.Add(error); + + var expectedJson = JsonConvert.SerializeObject(new + { + errors = new dynamic[] { + new { + myCustomProperty = "custom", + title = "title", + detail = "detail", + status = "507" + } + } + }); + var serializer = GetResponseSerializer(); + + // Act + var result = serializer.Serialize(errorCollection); + + // Assert + Assert.Equal(expectedJson, result); + } + + class CustomError : Error + { + public CustomError(int status, string title, string detail, string myProp) + : base(status, title, detail) + { + MyCustomProperty = myProp; + } + public string MyCustomProperty { get; set; } + } + } +} diff --git a/test/UnitTests/Services/EntityResourceService_Tests.cs b/test/UnitTests/Services/EntityResourceService_Tests.cs index 0643110b21..9834c2477c 100644 --- a/test/UnitTests/Services/EntityResourceService_Tests.cs +++ b/test/UnitTests/Services/EntityResourceService_Tests.cs @@ -1,7 +1,15 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Query; +using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.Extensions.Logging; @@ -12,67 +20,84 @@ namespace UnitTests.Services { public class EntityResourceService_Tests { - private readonly Mock _jsonApiContextMock = new Mock(); - private readonly Mock> _repositoryMock = new Mock>(); + private readonly Mock> _repositoryMock = new Mock>(); private readonly ILoggerFactory _loggerFactory = new Mock().Object; + private readonly Mock _crMock; + private readonly Mock _pgsMock; + private readonly Mock _ufMock; + private readonly IResourceGraph _resourceGraph; public EntityResourceService_Tests() { - _jsonApiContextMock - .Setup(m => m.ResourceGraph) - .Returns( - new ResourceGraphBuilder() - .AddResource("todo-items") - .Build() - ); + _crMock = new Mock(); + _pgsMock = new Mock(); + _ufMock = new Mock(); + _resourceGraph = new ResourceGraphBuilder() + .AddResource() + .AddResource() + .Build(); + } [Fact] public async Task GetRelationshipAsync_Passes_Public_ResourceName_To_Repository() { - // arrange + // Arrange const int id = 1; const string relationshipName = "collection"; + var relationship = new RelationshipAttribute[] { new HasOneAttribute(relationshipName) }; - _repositoryMock.Setup(m => m.GetAndIncludeAsync(id, relationshipName)) - .ReturnsAsync(new TodoItem()); + var todoItem = new TodoItem(); + var query = new List { todoItem }.AsQueryable(); - var repository = GetService(); + _repositoryMock.Setup(m => m.Get(id)).Returns(query); + _repositoryMock.Setup(m => m.Include(query, relationship)).Returns(query); + _repositoryMock.Setup(m => m.FirstOrDefaultAsync(query)).ReturnsAsync(todoItem); + + var service = GetService(); - // act - await repository.GetRelationshipAsync(id, relationshipName); + // Act + await service.GetRelationshipAsync(id, relationshipName); - // assert - _repositoryMock.Verify(m => m.GetAndIncludeAsync(id, relationshipName), Times.Once); + // Assert + _repositoryMock.Verify(m => m.Get(id), Times.Once); + _repositoryMock.Verify(m => m.Include(query, relationship), Times.Once); + _repositoryMock.Verify(m => m.FirstOrDefaultAsync(query), Times.Once); } [Fact] public async Task GetRelationshipAsync_Returns_Relationship_Value() { - // arrange + // Arrange const int id = 1; const string relationshipName = "collection"; + var relationship = new RelationshipAttribute[] { new HasOneAttribute(relationshipName) }; var todoItem = new TodoItem { Collection = new TodoItemCollection { Id = Guid.NewGuid() } }; - _repositoryMock.Setup(m => m.GetAndIncludeAsync(id, relationshipName)) - .ReturnsAsync(todoItem); + var query = new List { todoItem }.AsQueryable(); + + _repositoryMock.Setup(m => m.Get(id)).Returns(query); + _repositoryMock.Setup(m => m.Include(query, relationship)).Returns(query); + _repositoryMock.Setup(m => m.FirstOrDefaultAsync(query)).ReturnsAsync(todoItem); var repository = GetService(); - // act + // Act var result = await repository.GetRelationshipAsync(id, relationshipName); - // assert + // Assert Assert.NotNull(result); var collection = Assert.IsType(result); Assert.Equal(todoItem.Collection.Id, collection.Id); } - private EntityResourceService GetService() => - new EntityResourceService(_jsonApiContextMock.Object, _repositoryMock.Object, _loggerFactory, null); + private DefaultResourceService GetService() + { + return new DefaultResourceService(new List(), new JsonApiOptions(), _repositoryMock.Object, _resourceGraph); + } } } diff --git a/test/UnitTests/Services/Operations/OperationsProcessorResolverTests.cs b/test/UnitTests/Services/Operations/OperationsProcessorResolverTests.cs deleted file mode 100644 index 6ebfc0bda5..0000000000 --- a/test/UnitTests/Services/Operations/OperationsProcessorResolverTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Generics; -using JsonApiDotNetCore.Models.Operations; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCore.Services.Operations; -using Moq; -using Xunit; - -namespace UnitTests.Services -{ - public class OperationProcessorResolverTests - { - private readonly Mock _processorFactoryMock; - public readonly Mock _jsonApiContextMock; - - public OperationProcessorResolverTests() - { - _processorFactoryMock = new Mock(); - _jsonApiContextMock = new Mock(); - } - - [Fact] - public void LocateCreateService_Throws_400_For_Entity_Not_Registered() - { - // arrange - _jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(new ResourceGraphBuilder().Build()); - var service = GetService(); - var op = new Operation - { - Ref = new ResourceReference - { - Type = "non-existent-type" - } - }; - - // act, assert - var e = Assert.Throws(() => service.LocateCreateService(op)); - Assert.Equal(400, e.GetStatusCode()); - } - - [Fact] - public void LocateGetService_Throws_400_For_Entity_Not_Registered() - { - // arrange - _jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(new ResourceGraphBuilder().Build()); - var service = GetService(); - var op = new Operation - { - Ref = new ResourceReference - { - Type = "non-existent-type" - } - }; - - // act, assert - var e = Assert.Throws(() => service.LocateGetService(op)); - Assert.Equal(400, e.GetStatusCode()); - } - - [Fact] - public void LocateRemoveService_Throws_400_For_Entity_Not_Registered() - { - // arrange - _jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(new ResourceGraphBuilder().Build()); - var service = GetService(); - var op = new Operation - { - Ref = new ResourceReference - { - Type = "non-existent-type" - } - }; - - // act, assert - var e = Assert.Throws(() => service.LocateRemoveService(op)); - Assert.Equal(400, e.GetStatusCode()); - } - - [Fact] - public void LocateUpdateService_Throws_400_For_Entity_Not_Registered() - { - // arrange - _jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(new ResourceGraphBuilder().Build()); - var service = GetService(); - var op = new Operation - { - Ref = new ResourceReference - { - Type = "non-existent-type" - } - }; - - // act, assert - var e = Assert.Throws(() => service.LocateUpdateService(op)); - Assert.Equal(400, e.GetStatusCode()); - } - - private OperationProcessorResolver GetService() - => new OperationProcessorResolver(_processorFactoryMock.Object, _jsonApiContextMock.Object); - } -} diff --git a/test/UnitTests/Services/Operations/OperationsProcessorTests.cs b/test/UnitTests/Services/Operations/OperationsProcessorTests.cs deleted file mode 100644 index 08211c5a67..0000000000 --- a/test/UnitTests/Services/Operations/OperationsProcessorTests.cs +++ /dev/null @@ -1,197 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Models.Operations; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCore.Services.Operations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage; -using Moq; -using Newtonsoft.Json; -using Xunit; - -namespace UnitTests.Services -{ - public class OperationsProcessorTests - { - private readonly Mock _resolverMock; - public readonly Mock _dbContextMock; - public readonly Mock _dbContextResolverMock; - public readonly Mock _jsonApiContextMock; - - public OperationsProcessorTests() - { - _resolverMock = new Mock(); - _dbContextMock = new Mock(); - _dbContextResolverMock = new Mock(); - _jsonApiContextMock = new Mock(); - } - - [Fact] - public async Task ProcessAsync_Performs_LocalId_ReplacementAsync_In_Relationships() - { - // arrange - var request = @"[ - { - ""op"": ""add"", - ""data"": { - ""type"": ""authors"", - ""lid"": ""a"", - ""attributes"": { - ""name"": ""dgeb"" - } - } - }, { - ""op"": ""add"", - ""data"": { - ""type"": ""articles"", - ""attributes"": { - ""title"": ""JSON API paints my bikeshed!"" - }, - ""relationships"": { - ""author"": { - ""data"": { - ""type"": ""authors"", - ""lid"": ""a"" - } - } - } - } - } - ]"; - - var op1Result = @"{ - ""links"": { - ""self"": ""http://example.com/authors/9"" - }, - ""data"": { - ""type"": ""authors"", - ""id"": ""9"", - ""lid"": ""a"", - ""attributes"": { - ""name"": ""dgeb"" - } - } - }"; - - var operations = JsonConvert.DeserializeObject>(request); - var addOperationResult = JsonConvert.DeserializeObject(op1Result); - - var databaseMock = new Mock(_dbContextMock.Object); - var transactionMock = new Mock(); - databaseMock.Setup(m => m.BeginTransactionAsync(It.IsAny())) - .ReturnsAsync(transactionMock.Object); - _dbContextMock.Setup(m => m.Database).Returns(databaseMock.Object); - - var opProcessorMock = new Mock(); - opProcessorMock.Setup(m => m.ProcessAsync(It.Is(op => op.DataObject.Type.ToString() == "authors"))) - .ReturnsAsync(addOperationResult); - - _resolverMock.Setup(m => m.LocateCreateService(It.IsAny())) - .Returns(opProcessorMock.Object); - - _dbContextResolverMock.Setup(m => m.GetContext()).Returns(_dbContextMock.Object); - var operationsProcessor = new OperationsProcessor(_resolverMock.Object, _dbContextResolverMock.Object, _jsonApiContextMock.Object); - - // act - var results = await operationsProcessor.ProcessAsync(operations); - - // assert - opProcessorMock.Verify( - m => m.ProcessAsync( - It.Is(o => - o.DataObject.Type.ToString() == "articles" - && o.DataObject.Relationships["author"].SingleData.Id == "9" - ) - ) - ); - } - - [Fact] - public async Task ProcessAsync_Performs_LocalId_ReplacementAsync_In_References() - { - // arrange - var request = @"[ - { - ""op"": ""add"", - ""data"": { - ""type"": ""authors"", - ""lid"": ""a"", - ""attributes"": { - ""name"": ""jaredcnance"" - } - } - }, { - ""op"": ""update"", - ""ref"": { - ""type"": ""authors"", - ""lid"": ""a"" - }, - ""data"": { - ""type"": ""authors"", - ""lid"": ""a"", - ""attributes"": { - ""name"": ""jnance"" - } - } - } - ]"; - - var op1Result = @"{ - ""data"": { - ""type"": ""authors"", - ""id"": ""9"", - ""lid"": ""a"", - ""attributes"": { - ""name"": ""jaredcnance"" - } - } - }"; - - var operations = JsonConvert.DeserializeObject>(request); - var addOperationResult = JsonConvert.DeserializeObject(op1Result); - - var databaseMock = new Mock(_dbContextMock.Object); - var transactionMock = new Mock(); - - databaseMock.Setup(m => m.BeginTransactionAsync(It.IsAny())) - .ReturnsAsync(transactionMock.Object); - - _dbContextMock.Setup(m => m.Database).Returns(databaseMock.Object); - - // setup add - var addOpProcessorMock = new Mock(); - addOpProcessorMock.Setup(m => m.ProcessAsync(It.Is(op => op.DataObject.Type.ToString() == "authors"))) - .ReturnsAsync(addOperationResult); - _resolverMock.Setup(m => m.LocateCreateService(It.IsAny())) - .Returns(addOpProcessorMock.Object); - - // setup update - var updateOpProcessorMock = new Mock(); - updateOpProcessorMock.Setup(m => m.ProcessAsync(It.Is(op => op.DataObject.Type.ToString() == "authors"))) - .ReturnsAsync((Operation)null); - _resolverMock.Setup(m => m.LocateUpdateService(It.IsAny())) - .Returns(updateOpProcessorMock.Object); - - _dbContextResolverMock.Setup(m => m.GetContext()).Returns(_dbContextMock.Object); - var operationsProcessor = new OperationsProcessor(_resolverMock.Object, _dbContextResolverMock.Object, _jsonApiContextMock.Object); - - // act - var results = await operationsProcessor.ProcessAsync(operations); - - // assert - updateOpProcessorMock.Verify( - m => m.ProcessAsync( - It.Is(o => - o.DataObject.Type.ToString() == "authors" - // && o.DataObject.Id == "9" // currently, we will not replace the data.id member - && o.DataObject.Id == null - && o.Ref.Id == "9" - ) - ) - ); - } - } -} diff --git a/test/UnitTests/Services/Operations/Processors/CreateOpProcessorTests.cs b/test/UnitTests/Services/Operations/Processors/CreateOpProcessorTests.cs deleted file mode 100644 index 73f9c272b9..0000000000 --- a/test/UnitTests/Services/Operations/Processors/CreateOpProcessorTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Operations; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Services; -using JsonApiDotNetCore.Services.Operations.Processors; -using Moq; -using Xunit; - -namespace UnitTests.Services -{ - public class CreateOpProcessorTests - { - private readonly Mock> _createServiceMock; - private readonly Mock _deserializerMock; - private readonly Mock _documentBuilderMock; - - public CreateOpProcessorTests() - { - _createServiceMock = new Mock>(); - _deserializerMock = new Mock(); - _documentBuilderMock = new Mock(); - } - - [Fact] - public async Task ProcessAsync_Deserializes_And_Creates() - { - // arrange - var testResource = new TestResource { - Name = "some-name" - }; - - var data = new ResourceObject { - Type = "test-resources", - Attributes = new Dictionary { - { "name", testResource.Name } - } - }; - - var operation = new Operation { - Data = data, - }; - - var resourceGraph = new ResourceGraphBuilder() - .AddResource("test-resources") - .Build(); - - _deserializerMock.Setup(m => m.DocumentToObject(It.IsAny(), It.IsAny>())) - .Returns(testResource); - - var opProcessor = new CreateOpProcessor( - _createServiceMock.Object, - _deserializerMock.Object, - _documentBuilderMock.Object, - resourceGraph - ); - - _documentBuilderMock.Setup(m => m.GetData(It.IsAny(), It.IsAny())) - .Returns(data); - - // act - var result = await opProcessor.ProcessAsync(operation); - - // assert - Assert.Equal(OperationCode.add, result.Op); - Assert.NotNull(result.Data); - Assert.Equal(testResource.Name, result.DataObject.Attributes["name"]); - _createServiceMock.Verify(m => m.CreateAsync(It.IsAny())); - } - - public class TestResource : Identifiable - { - [Attr("name")] - public string Name { get; set; } - } - } -} diff --git a/test/UnitTests/Services/QueryAccessorTests.cs b/test/UnitTests/Services/QueryAccessorTests.cs deleted file mode 100644 index aa8bc6ae7e..0000000000 --- a/test/UnitTests/Services/QueryAccessorTests.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Collections.Generic; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Query; -using JsonApiDotNetCore.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; - -namespace UnitTests.Services -{ - public class QueryAccessorTests - { - private readonly Mock _contextMock; - private readonly Mock> _loggerMock; - private readonly Mock _queryMock; - - public QueryAccessorTests() - { - _contextMock = new Mock(); - _loggerMock = new Mock>(); - _queryMock = new Mock(); - } - - [Fact] - public void Can_Get_Guid_QueryValue() - { - // arrange - const string key = "SomeId"; - var value = Guid.NewGuid(); - var querySet = new QuerySet - { - Filters = new List { - new FilterQuery(key, value.ToString(), "eq") - } - }; - - _contextMock.Setup(c => c.QuerySet).Returns(querySet); - - var service = new QueryAccessor(_contextMock.Object, _loggerMock.Object); - - // act - var success = service.TryGetValue("SomeId", out Guid result); - - // assert - Assert.True(success); - Assert.Equal(value, result); - } - - [Fact] - public void GetRequired_Throws_If_Not_Present() - { - // arrange - const string key = "SomeId"; - var value = Guid.NewGuid(); - - var querySet = new QuerySet - { - Filters = new List { - new FilterQuery(key, value.ToString(), "eq") - } - }; - - _contextMock.Setup(c => c.QuerySet).Returns(querySet); - - var service = new QueryAccessor(_contextMock.Object, _loggerMock.Object); - - // act - var exception = Assert.Throws(() => service.GetRequired("Invalid")); - - // assert - Assert.Equal(422, exception.GetStatusCode()); - } - - [Fact] - public void GetRequired_Does_Not_Throw_If_Present() - { - // arrange - const string key = "SomeId"; - var value = Guid.NewGuid(); - - var querySet = new QuerySet - { - Filters = new List { - new FilterQuery(key, value.ToString(), "eq") - } - }; - - _contextMock.Setup(c => c.QuerySet).Returns(querySet); - - var service = new QueryAccessor(_contextMock.Object, _loggerMock.Object); - - // act - var result = service.GetRequired("SomeId"); - - // assert - Assert.Equal(value, result); - } - } -} diff --git a/test/UnitTests/Services/QueryComposerTests.cs b/test/UnitTests/Services/QueryComposerTests.cs deleted file mode 100644 index efa600f2f3..0000000000 --- a/test/UnitTests/Services/QueryComposerTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Internal.Query; -using JsonApiDotNetCore.Services; -using Moq; -using Xunit; - -namespace UnitTests.Services -{ - public class QueryComposerTests - { - private readonly Mock _jsonApiContext; - - public QueryComposerTests() - { - _jsonApiContext = new Mock(); - } - - [Fact] - public void Can_ComposeEqual_FilterStringForUrl() - { - // arrange - var filter = new FilterQuery("attribute", "value", "eq"); - var querySet = new QuerySet(); - List filters = new List(); - filters.Add(filter); - querySet.Filters = filters; - - _jsonApiContext - .Setup(m => m.QuerySet) - .Returns(querySet); - - var queryComposer = new QueryComposer(); - // act - var filterString = queryComposer.Compose(_jsonApiContext.Object); - // assert - Assert.Equal("&filter[attribute]=eq:value", filterString); - } - - [Fact] - public void Can_ComposeLessThan_FilterStringForUrl() - { - // arrange - var filter = new FilterQuery("attribute", "value", "le"); - var filter2 = new FilterQuery("attribute2", "value2", ""); - var querySet = new QuerySet(); - List filters = new List(); - filters.Add(filter); - filters.Add(filter2); - querySet.Filters = filters; - - _jsonApiContext - .Setup(m => m.QuerySet) - .Returns(querySet); - - var queryComposer = new QueryComposer(); - // act - var filterString = queryComposer.Compose(_jsonApiContext.Object); - // assert - Assert.Equal("&filter[attribute]=le:value&filter[attribute2]=value2", filterString); - } - - [Fact] - public void NoFilter_Compose_EmptyStringReturned() - { - // arrange - var querySet = new QuerySet(); - - _jsonApiContext - .Setup(m => m.QuerySet) - .Returns(querySet); - - var queryComposer = new QueryComposer(); - // act - var filterString = queryComposer.Compose(_jsonApiContext.Object); - // assert - Assert.Equal("", filterString); - } - } -} diff --git a/test/UnitTests/Services/QueryParserTests.cs b/test/UnitTests/Services/QueryParserTests.cs deleted file mode 100644 index 58cd6251e9..0000000000 --- a/test/UnitTests/Services/QueryParserTests.cs +++ /dev/null @@ -1,383 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; -using Moq; -using Xunit; - -namespace UnitTests.Services -{ - public class QueryParserTests - { - private readonly Mock _controllerContextMock; - private readonly Mock _queryCollectionMock; - - public QueryParserTests() - { - _controllerContextMock = new Mock(); - _queryCollectionMock = new Mock(); - } - - [Fact] - public void Can_Build_Filters() - { - // arrange - var query = new Dictionary { - { "filter[key]", new StringValues("value") } - }; - - _queryCollectionMock - .Setup(m => m.GetEnumerator()) - .Returns(query.GetEnumerator()); - - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.None)); - - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); - - // act - var querySet = queryParser.Parse(_queryCollectionMock.Object); - - // assert - Assert.Equal("value", querySet.Filters.Single(f => f.Attribute == "key").Value); - } - - [Fact] - public void Filters_Properly_Parses_DateTime_With_Operation() - { - // arrange - const string dt = "2017-08-15T22:43:47.0156350-05:00"; - var query = new Dictionary { - { "filter[key]", new StringValues("le:" + dt) } - }; - - _queryCollectionMock - .Setup(m => m.GetEnumerator()) - .Returns(query.GetEnumerator()); - - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.None)); - - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); - - // act - var querySet = queryParser.Parse(_queryCollectionMock.Object); - - // assert - Assert.Equal(dt, querySet.Filters.Single(f => f.Attribute == "key").Value); - Assert.Equal("le", querySet.Filters.Single(f => f.Attribute == "key").Operation); - } - - [Fact] - public void Filters_Properly_Parses_DateTime_Without_Operation() - { - // arrange - const string dt = "2017-08-15T22:43:47.0156350-05:00"; - var query = new Dictionary { - { "filter[key]", new StringValues(dt) } - }; - - _queryCollectionMock - .Setup(m => m.GetEnumerator()) - .Returns(query.GetEnumerator()); - - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.None)); - - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); - - // act - var querySet = queryParser.Parse(_queryCollectionMock.Object); - - // assert - Assert.Equal(dt, querySet.Filters.Single(f => f.Attribute == "key").Value); - Assert.Equal(string.Empty, querySet.Filters.Single(f => f.Attribute == "key").Operation); - } - - [Fact] - public void Can_Disable_Filters() - { - // arrange - var query = new Dictionary { - { "filter[key]", new StringValues("value") } - }; - - _queryCollectionMock - .Setup(m => m.GetEnumerator()) - .Returns(query.GetEnumerator()); - - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.Filter)); - - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); - - // act - var querySet = queryParser.Parse(_queryCollectionMock.Object); - - // assert - Assert.Empty(querySet.Filters); - } - [Theory] - [InlineData("text,,1")] - [InlineData("text,hello,,5")] - [InlineData(",,2")] - public void Parse_EmptySortSegment_ReceivesJsonApiException(string stringSortQuery) - { - // Arrange - var query = new Dictionary { - { "sort", new StringValues(stringSortQuery) } - }; - - _queryCollectionMock - .Setup(m => m.GetEnumerator()) - .Returns(query.GetEnumerator()); - - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); - - // Act / Assert - var exception = Assert.Throws(() => - { - var querySet = queryParser.Parse(_queryCollectionMock.Object); - }); - Assert.Contains("sort", exception.Message); - } - [Fact] - public void Can_Disable_Sort() - { - // Arrange - var query = new Dictionary { - { "sort", new StringValues("-key") } - }; - - _queryCollectionMock - .Setup(m => m.GetEnumerator()) - .Returns(query.GetEnumerator()); - - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.Sort)); - - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); - - // act - var querySet = queryParser.Parse(_queryCollectionMock.Object); - - // assert - Assert.Empty(querySet.SortParameters); - } - - [Fact] - public void Can_Disable_Include() - { - // arrange - var query = new Dictionary { - { "include", new StringValues("key") } - }; - - _queryCollectionMock - .Setup(m => m.GetEnumerator()) - .Returns(query.GetEnumerator()); - - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.Include)); - - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); - - // act - var querySet = queryParser.Parse(_queryCollectionMock.Object); - - // assert - Assert.Empty(querySet.IncludedRelationships); - } - - [Fact] - public void Can_Disable_Page() - { - // arrange - var query = new Dictionary { - { "page[size]", new StringValues("1") } - }; - - _queryCollectionMock - .Setup(m => m.GetEnumerator()) - .Returns(query.GetEnumerator()); - - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.Page)); - - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); - - // act - var querySet = queryParser.Parse(_queryCollectionMock.Object); - - // assert - Assert.Equal(0, querySet.PageQuery.PageSize); - } - - [Fact] - public void Can_Disable_Fields() - { - // arrange - var query = new Dictionary { - { "fields", new StringValues("key") } - }; - - _queryCollectionMock - .Setup(m => m.GetEnumerator()) - .Returns(query.GetEnumerator()); - - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.Fields)); - - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); - - // act - var querySet = queryParser.Parse(_queryCollectionMock.Object); - - // assert - Assert.Empty(querySet.Fields); - } - - [Fact] - public void Can_Parse_Fields_Query() - { - // arrange - const string type = "articles"; - const string attrName = "some-field"; - const string internalAttrName = "SomeField"; - - var query = new Dictionary { { $"fields[{type}]", new StringValues(attrName) } }; - - _queryCollectionMock - .Setup(m => m.GetEnumerator()) - .Returns(query.GetEnumerator()); - - _controllerContextMock - .Setup(m => m.RequestEntity) - .Returns(new ContextEntity - { - EntityName = type, - Attributes = new List - { - new AttrAttribute(attrName) - { - InternalAttributeName = internalAttrName - } - }, - Relationships = new List() - }); - - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); - - // act - var querySet = queryParser.Parse(_queryCollectionMock.Object); - - // assert - Assert.NotEmpty(querySet.Fields); - Assert.Equal(2, querySet.Fields.Count); - Assert.Equal("Id", querySet.Fields[0]); - Assert.Equal(internalAttrName, querySet.Fields[1]); - } - - [Fact] - public void Throws_JsonApiException_If_Field_DoesNotExist() - { - // arrange - const string type = "articles"; - const string attrName = "dne"; - - var query = new Dictionary { { $"fields[{type}]", new StringValues(attrName) } }; - - _queryCollectionMock - .Setup(m => m.GetEnumerator()) - .Returns(query.GetEnumerator()); - - _controllerContextMock - .Setup(m => m.RequestEntity) - .Returns(new ContextEntity - { - EntityName = type, - Attributes = new List(), - Relationships = new List() - }); - - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); - - // act , assert - var ex = Assert.Throws(() => queryParser.Parse(_queryCollectionMock.Object)); - Assert.Equal(400, ex.GetStatusCode()); - } - - [Theory] - [InlineData("1", 1, false)] - [InlineData("abcde", 0, true)] - [InlineData("", 0, true)] - public void Can_Parse_Page_Size_Query(string value, int expectedValue, bool shouldThrow) - { - // arrange - var query = new Dictionary - { { "page[size]", new StringValues(value) } - }; - - _queryCollectionMock - .Setup(m => m.GetEnumerator()) - .Returns(query.GetEnumerator()); - - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); - - // act - if (shouldThrow) - { - var ex = Assert.Throws(() => queryParser.Parse(_queryCollectionMock.Object)); - Assert.Equal(400, ex.GetStatusCode()); - } - else - { - var querySet = queryParser.Parse(_queryCollectionMock.Object); - Assert.Equal(expectedValue, querySet.PageQuery.PageSize); - } - } - - [Theory] - [InlineData("1", 1, false)] - [InlineData("abcde", 0, true)] - [InlineData("", 0, true)] - public void Can_Parse_Page_Number_Query(string value, int expectedValue, bool shouldThrow) - { - // arrange - var query = new Dictionary - { { "page[number]", new StringValues(value) } - }; - - _queryCollectionMock - .Setup(m => m.GetEnumerator()) - .Returns(query.GetEnumerator()); - - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); - - // act - if (shouldThrow) - { - var ex = Assert.Throws(() => queryParser.Parse(_queryCollectionMock.Object)); - Assert.Equal(400, ex.GetStatusCode()); - } - else - { - var querySet = queryParser.Parse(_queryCollectionMock.Object); - Assert.Equal(expectedValue, querySet.PageQuery.PageOffset); - } - } - } -} diff --git a/test/UnitTests/TestModels.cs b/test/UnitTests/TestModels.cs new file mode 100644 index 0000000000..8abcc84b8e --- /dev/null +++ b/test/UnitTests/TestModels.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace UnitTests.TestModels +{ + + public class TestResource : Identifiable + { + [Attr] public string StringField { get; set; } + [Attr] public DateTime DateTimeField { get; set; } + [Attr] public DateTime? NullableDateTimeField { get; set; } + [Attr] public int IntField { get; set; } + [Attr] public int? NullableIntField { get; set; } + [Attr] public Guid GuidField { get; set; } + [Attr] public ComplexType ComplexField { get; set; } + [Attr(isImmutable: true)] public string Immutable { get; set; } + } + + public class TestResourceWithList : Identifiable + { + [Attr] public List ComplexFields { get; set; } + } + + public class ComplexType + { + public string CompoundName { get; set; } + } + + public class OneToOnePrincipal : IdentifiableWithAttribute + { + [HasOne] public OneToOneDependent Dependent { get; set; } + } + + public class OneToOneDependent : IdentifiableWithAttribute + { + [HasOne] public OneToOnePrincipal Principal { get; set; } + public int? PrincipalId { get; set; } + } + + public class OneToOneRequiredDependent : IdentifiableWithAttribute + { + [HasOne] public OneToOnePrincipal Principal { get; set; } + public int PrincipalId { get; set; } + } + + public class OneToManyDependent : IdentifiableWithAttribute + { + [HasOne] public OneToManyPrincipal Principal { get; set; } + public int? PrincipalId { get; set; } + } + + public class OneToManyRequiredDependent : IdentifiableWithAttribute + { + [HasOne] public OneToManyPrincipal Principal { get; set; } + public int PrincipalId { get; set; } + } + + public class OneToManyPrincipal : IdentifiableWithAttribute + { + [HasMany] public List Dependents { get; set; } + } + + public class IdentifiableWithAttribute : Identifiable + { + [Attr] public string AttributeMember { get; set; } + } + + public class MultipleRelationshipsPrincipalPart : IdentifiableWithAttribute + { + [HasOne] public OneToOneDependent PopulatedToOne { get; set; } + [HasOne] public OneToOneDependent EmptyToOne { get; set; } + [HasMany] public List PopulatedToManies { get; set; } + [HasMany] public List EmptyToManies { get; set; } + [HasOne] public MultipleRelationshipsPrincipalPart Multi { get; set; } + } + + public class MultipleRelationshipsDependentPart : IdentifiableWithAttribute + { + [HasOne] public OneToOnePrincipal PopulatedToOne { get; set; } + public int PopulatedToOneId { get; set; } + [HasOne] public OneToOnePrincipal EmptyToOne { get; set; } + public int? EmptyToOneId { get; set; } + [HasOne] public OneToManyPrincipal PopulatedToMany { get; set; } + public int PopulatedToManyId { get; set; } + [HasOne] public OneToManyPrincipal EmptyToMany { get; set; } + public int? EmptyToManyId { get; set; } + } + + public class Article : Identifiable + { + [Attr] public string Title { get; set; } + [HasOne] public Person Reviewer { get; set; } + [HasOne] public Person Author { get; set; } + + [HasOne(canInclude: false)] public Person CannotInclude { get; set; } + } + + public class Person : Identifiable + { + [Attr] public string Name { get; set; } + [HasMany] public List Blogs { get; set; } + [HasOne] public Food FavoriteFood { get; set; } + [HasOne] public Song FavoriteSong { get; set; } + } + + public class Blog : Identifiable + { + [Attr] public string Title { get; set; } + [HasOne] public Person Reviewer { get; set; } + [HasOne] public Person Author { get; set; } + } + + public class Food : Identifiable + { + [Attr] public string Dish { get; set; } + } + + public class Song : Identifiable + { + [Attr] public string Title { get; set; } + } + +} diff --git a/test/UnitTests/UnitTests.csproj b/test/UnitTests/UnitTests.csproj index b4f96631ca..7374a96ae6 100644 --- a/test/UnitTests/UnitTests.csproj +++ b/test/UnitTests/UnitTests.csproj @@ -8,12 +8,13 @@ - - + + + - + @@ -22,9 +23,10 @@ - - - - + + + + + diff --git a/wiki/v4/content/deprecation.md b/wiki/v4/content/deprecation.md new file mode 100644 index 0000000000..65caab195b --- /dev/null +++ b/wiki/v4/content/deprecation.md @@ -0,0 +1,5 @@ +# Deprecation + +* Bulk +* Operations +* Resource entity seperation diff --git a/wiki/v4/content/query-parameter-services.md b/wiki/v4/content/query-parameter-services.md new file mode 100644 index 0000000000..28aba58f51 --- /dev/null +++ b/wiki/v4/content/query-parameter-services.md @@ -0,0 +1,123 @@ +# Query Parameter Services + +This article describes +1. how URL query parameters are currently processed internally +2. how to customize the behaviour of existing query parameters +3. how to register your own + +## 1. Internal usage + +Below is a list of the query parameters that are supported. Each supported query parameter has it's own dedicated service. + +| Query Parameter Service | Occurence in URL | Domain | +|-------------------------|--------------------------------|-------------------------------------------------------| +| `IFilterService` | `?filter[article.title]=title` | filtering the resultset | +| `IIncludeService` | `?include=article.author` | including related data | +| `IPageService` | `?page[size]=10&page[number]=3` | pagination of the resultset | +| `ISortService` | `?sort=-title` | sorting the resultset | +| `ISparseFieldsService` | `?fields[article]=title,summary` | sparse field selection | +| `IOmitDefaultService` | `?omitDefault=true` | omitting default values from the serialization result, eg `guid-value": "00000000-0000-0000-0000-000000000000"` | +| `IOmitNullService` | `?omitNull=false` | omitting null values from the serialization result | + + +These services are responsible for parsing the value from the URL by gathering relevant (meta)data and performing validations as required by JsonApiDotNetCore down the pipeline. For example, the `IIncludeService` is responsible for checking if `article.author` is a valid relationship chain, and pre-processes the chain into a `List` so that the rest of the framework can process it easier. + +Each of these services implement the `IQueryParameterService` interface, which exposes: +* a `Name` property that is used internally to match the URL query parameter to the service. + `IIncludeService.Name` returns `include`, which will match `include=article.author` +* a `Parse` method that is called internally in the middleware to process the url query parameters. + + +```c# +public interface IQueryParameterService +{ + /// + /// Parses the value of the query parameter. Invoked in the middleware. + /// + /// the value of the query parameter as retrieved from the url + void Parse(KeyValuePair queryParameter); + /// + /// The name of the query parameter as matched in the URL query string. + /// + string Name { get; } +} +``` + +The piece of internals that is responsible for calling the `Parse` method is the `IQueryParameterDiscovery` service (formally known as `QueryParser`). This service injects every registered implementation of `IQueryParameterService` and calls the parse method with the appropiate part of the url querystring. + + +## 2. Customizing behaviour +You can register your own implementation of every service interface in the table above. As an example, we may want to add additional support for `page[index]=3` next to `page[number]=3` ("number" replaced with "index"). This could be achieved as follows + +```c# +// CustomPageService.cs +public class CustomPageService : PageService +{ + public override void Parse(KeyValuePair queryParameter) + { + var key = queryParameter.Key.Replace("index", "number"); + queryParameter = KeyValuePair(key, queryParameter.Value); + base.Parse(queryParameter) + } +} + +// Startup.cs +services.AddScoped(); +``` + +## 3. Registering new services +You may also define an entirely new custom query parameter. For example, we want to trigger a `HTTP 418 I'm a teapot` if a client includes a `?teapot=true` query parameter. This could be implemented as follows: + + +```c# +// ITeapotService.cs +public interface ITeapotService +{ + // Interface containing the "business logic" of the query parameter service, + // in a way useful to your application + bool ShouldThrowTeapot { get; } +} + +// TeapotService.cs +public class TeapotService : IQueryParameterService, ITeapotService +{ // ^^^ must inherit the IQueryParameterService interface + pubic bool ShouldThrowTeapot { get; } + + public string Name => "teapot"; + + public override void Parse(KeyValuePair queryParameter) + { + if(bool.Parse(queryParameter.Value, out bool config)) + ShouldThrowTeapot = true; + } +} + +// Startup.cs +services.AddScoped(); // exposes the parsed query parameter to your application +services.AddScoped(); // ensures that the associated query parameter service will be parsed internally by JADNC. +``` + +Things to pay attention to: +* The teapot service must be registered as an implementation of `IQueryParameterService` to be processed internally in the middleware +* Any other (business) logic is exposed on ITeapotService for usage in your application. + + +Now, we could access the custom query parameter service anywhere in our application to trigger a 418. Let's use the resource hooks to include this piece of business logic +```c# +public class TodoResource : ResourceDefinition +{ + private readonly ITeapotService _teapotService; + + public TodoResource(IResourceGraph graph, ITeapotService teapotService) : base(graph) + { + _teapotService = teapotService + } + + public override void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) + { + if (teapotService.ShouldThrowTeapot) + throw new JsonApiException(418, "This is caused by the usage of teapot=true.") + } + +} +``` \ No newline at end of file diff --git a/wiki/v4/content/serialization.md b/wiki/v4/content/serialization.md new file mode 100644 index 0000000000..5abc4b2920 --- /dev/null +++ b/wiki/v4/content/serialization.md @@ -0,0 +1,111 @@ + +# Serialization + +The main change for serialization is that we have split the serialization responsibilities into two parts: + +* **Response (de)serializers** - (de)Serialization regarding serving or interpreting a response. +* **Request (de)serializer** - (de)Serialization regarding creating or interpreting a request. + +This split is done because during deserialization, some parts are relevant only for *client*-side parsing whereas others are only for *server*-side parsing. for example, a server deserializer will never have to deal with a `included` object list. Similarly, in serialization, a client serializer will for example never ever have to populate any other top-level members than the primary data (like `meta`, `included`). + +Throughout the document and the code when referring to fields, members, object types, the technical language of json:api spec is used. At the core of (de)serialization is the +`Document` class, [see document spec](https://jsonapi.org/format/#document-structure). + +## Changes + +In this section we will detail the changes made to the (de)serialization compared to the previous version. + +### Deserialization + +The previous `JsonApiDeSerializer` implementation is now split into a `RequestDeserializer` and `ResponseDeserializer`. Both inherit from `BaseDocumentParser` which does the shared parsing. + +#### BaseDocumentParser + +This (base) class is responsible for: + +* Converting the serialized string content into an intance of the `Document` class. Which is the most basic version of JSON API which has a `Data`, `Meta` and `Included` property. +* Building instances of the corresponding resource class (eg `Article`) by going through the document's primary data (`Document.Data`) For the spec for this: [Document spec](https://jsonapi.org/format/#document-top-level). + +Responsibility of any implementation the base class-specific parsing is shifted through the abstract `BaseDocumentParser.AfterProcessField()` method. This method is fired once each time after a `AttrAttribute` or `RelationshipAttribute` is processed. It allows a implementation of `BaseDocumentParser` to intercept the parsing and add steps that are only required for new implementations. + +#### ResponseDeserializer + +The client deserializer complements the base deserialization by + +* overriding the `AfterProcessField` method which takes care of the Included section \* after a relationship was deserialized, it finds the appended included object and adds it attributs and (nested) relationships +* taking care of remaining top-level members. that are only relevant to a client-side parser (meta data, server-side errors, links). + +#### RequestDeserializer + +For server-side parsing, no extra parsing needs to be done after the base deserialization is completed. It only needs to keep track of which `AttrAttribute`s and `RelationshipAttribute`s were targeted by a request. This is needed for the internals of JADNC (eg the repository layer). + +* The `AfterProcessField` method is overriden so that every attribute and relationship is registered with the `ITargetedFields` service after it is processed. + +## Serialization + +Like with the deserializers, `JsonApiSerializer` is now split up into these classes (indentation implies hierarchy/extending): + +* `IncludedResourceObjectBuilder` + +* `ResourceObjectBuilder` - *abstract* + * `DocumentBuilder` - *abstract* - + * `ResponseSerializer` + * `RequestSerializer` + +### ResourceObjectBuilder + +At the core of serialization is the `ResourceObject` class [see resource object spec](https://jsonapi.org/format/#document-resource-objects). + +ResourceObjectBuilder is responsible for Building a `ResourceObject` from an entity given a list of `AttrAttribute`s and `RelationshipAttribute`s. - Note: the resource object builder is NOT responsible for figuring out which attributes and relationships should be included in the serialization result, because this differs depending on an the implementation being client or server side. Instead, it is provided with the list. + +Additionally, client and server serializers also differ in how relationship members ([see relationship member spec](https://jsonapi.org/format/#document-resource-object-attributes) are formatted. The responsibility for handling this is again shifted, this time by virtual `ResourceObjectBuilder.GetRelationshipData()` method. This method is fired once each time a `RelationshipAttribute` is processed, allowing for additional serialization (like adding links or metadata). + +This time, the `GetRelationshipData()` method is not abstract, but virtual with a default implementation. This default implementation is to just create a `RelationshipData` with primary data (like `{"related-foo": { "data": { "id": 1" "type": "foobar"}}}`). Some implementations (server, included builder) need additional logic, others don't (client). + +### BaseDocumentBuilder +Responsible for + +- Calling the base resource object serialization for one (or many) entities and wrapping the result in a `Document`. + +Thats all. It does not figure out which attributes or relationships are to be serialized. + +### RequestSerializer + +Responsible for figuring out which attributes and relationships need to be serialized and calling the base document builder with that. +For example: + +- for a POST request, this is often (almost) all attributes. +- for a PATCH request, this is usually a small subset of attributes. + +Note that the client serializer is relatively skinny, because no top-level data (included, meta, links) will ever have to be added anywhere in the document. + +### ResponseSerializer + +Responsible for figuring out which attributes and relationships need to be serialized and calling the base document builder with that. +For example, for a GET request, all attributes are usually included in the output, unless + +* Sparse field selection was applied in the client request +* Runtime attribute hiding was applied, see [JADNC docs](https://json-api-dotnet.github.io/JsonApiDotNetCore/usage/resources/resource-definitions.html#runtime-attribute-filtering) + +The server serializer is also responsible for adding top-level meta data and links and appending included relationships. For this the `GetRelationshipData()` is overriden: + +* it adds links to the `RelationshipData` object (if configured to do so, see `ILinksConfiguration`). +* it checks if the processed relationship needs to be enclosed in the `included` list. If so, it calls the `IIncludedResourceObjectBuilder` to take care of that. + +### IncludedResourceObjectBuilder +Responsible for building the *included member* of a `Document`. Note that `IncludedResourceObjectBuilder` extends `ResourceObjectBuilder` and not `BaseDocumentBuilder` because it does not need to build an entire document but only resource objects. + +Responsible for building the _included member_ of a `Document`. Note that `IncludedResourceObjectBuilder` extends `ResourceObjectBuilder` and not `DocumentBuilder` because it does not need to build an entire document but only resource objects. + +Relationship _inclusion chains_ are at the core of building the included member. For example, consider the request `articles?included=author.blogs.reviewers.favorite-food,reviewer.blogs.author.favorite-song`. It contains the following (complex) inclusion chains: + +1. `author.blogs.reviewers.favorite-food` +2. `reviewer.blogs.author.favorite-song` + +Like with the `RequestSerializer` and `ResponseSerializer`, the `IncludedResourceObjectBuilder` is responsible for calling the base resource object builder with the list of attributes and relationships. For this implementation, these lists depend strongly on the inclusion chains. The above complex example demonstrates this (note: in this example the relationships `author` and `reviewer` are of the same resource `people`): + +* people that were included as reviewers from inclusion chain (1) should come with their `favorite-food` included, but not those from chain (2) +* people that were included as authors from inclusion chain (2) should come with their `favorite-song` included, but not those from chain (1). +* a person that was included as both an reviewer and author (i.e. targeted by both chain (1) and (2)), both `favorite-food` and `favorite-song` need to be present. + +To achieve this all of this, the `IncludedResourceObjectBuilder` needs to recursively parse an inclusion chain and make sure it does not append the same included more than once. This strategy is different from that of the ResponseSerializer, and for that reason it is a separate service. diff --git a/wiki/v4/decoupling-architecture.md b/wiki/v4/decoupling-architecture.md new file mode 100644 index 0000000000..8091fc8723 --- /dev/null +++ b/wiki/v4/decoupling-architecture.md @@ -0,0 +1,9 @@ +# V4 Architecture overview + +We upgraded to .NET Core 3.0. Furthermore, for V4 we have some explaining to do, namely the most notable changes: + +- [Serialization](./content/serialization.md) +- [Query Parameter Services](./content/query-parameter-services.md) +- [Extendability](./content/extendability.md) +- [Testing](./content/testing.md) +- [Deprecation](./content/deprecation.md)