diff --git a/Directory.Build.props b/Directory.Build.props index 0d034e0c5d..fcd668b5ba 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,14 +4,14 @@ netcoreapp2.0 netstandard2.0 - 2.1.0 + 2.* - 2.1.0 - 2.1.0 - 2.1.0 + 2.* + 2.* + 2.* - 2.1.0 - 2.1.0 + 2.* + 2.* 4.0.0 2.1.0 diff --git a/JsonApiDotnetCore.sln b/JsonApiDotnetCore.sln index a0330ce005..a0be085cb2 100644 --- a/JsonApiDotnetCore.sln +++ b/JsonApiDotnetCore.sln @@ -2,16 +2,10 @@ 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 @@ -23,31 +17,37 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NoEntityFrameworkExample", "src\Examples\NoEntityFrameworkExample\NoEntityFrameworkExample.csproj", "{570165EC-62B5-4684-A139-8D2A30DD4475}" +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}") = "NoEntityFrameworkTests", "test\NoEntityFrameworkTests\NoEntityFrameworkTests.csproj", "{73DA578D-A63F-4956-83ED-6D7102E09140}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore", "src\JsonApiDotNetCore\JsonApiDotNetCore.csproj", "{9D36BE59-7C14-448B-984D-93A0E7816314}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "test\UnitTests\UnitTests.csproj", "{6D4BD85A-A262-44C6-8572-FE3A30410BF3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCoreExample", "src\Examples\JsonApiDotNetCoreExample\JsonApiDotNetCoreExample.csproj", "{27FA7CD3-8DCD-4104-9AB4-B2D927F421B5}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{026FBC6C-AF76-4568-9B87-EC73457899FD}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoEntityFrameworkExample", "src\Examples\NoEntityFrameworkExample\NoEntityFrameworkExample.csproj", "{99BAF03C-362B-41FA-9FFF-67F697EFC28C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReportsExample", "src\Examples\ReportsExample\ReportsExample.csproj", "{FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReportsExample", "src\Examples\ReportsExample\ReportsExample.csproj", "{1CC0831C-ED1D-442E-8421-331D50BD41F1}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{076E1AE4-FD25-4684-B826-CAAE37FEA0AA}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OperationsExample", "src\Examples\OperationsExample\OperationsExample.csproj", "{3AB43764-C57A-4B75-8C03-C671D3925BF3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResourceEntitySeparationExample", "src\Examples\ResourceEntitySeparationExample\ResourceEntitySeparationExample.csproj", "{623792C0-5B7D-4D7D-A276-73F908FD4C34}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCoreExampleTests", "test\JsonApiDotNetCoreExampleTests\JsonApiDotNetCoreExampleTests.csproj", "{CAF331F8-9255-4D72-A1A8-A54141E99F1E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "benchmarks\Benchmarks.csproj", "{1F604666-BB0F-413E-922D-9D37C6073285}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoEntityFrameworkTests", "test\NoEntityFrameworkTests\NoEntityFrameworkTests.csproj", "{4F15A8F8-5BC6-45A1-BC51-03F921B726A4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OperationsExample", "src\Examples\OperationsExample\OperationsExample.csproj", "{CF2C1EB6-8449-4B35-B8C7-F43D6D90632D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "test\UnitTests\UnitTests.csproj", "{8788FF65-C2B6-40B2-A3A0-1E3D91C02664}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OperationsExampleTests", "test\OperationsExampleTests\OperationsExampleTests.csproj", "{9CD2C116-D133-4FE4-97DA-A9FEAFF045F1}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OperationsExampleTests", "test\OperationsExampleTests\OperationsExampleTests.csproj", "{65BF5960-3D9B-4230-99F4-A12CAA130792}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ResourceEntitySeparationExample", "src\Examples\ResourceEntitySeparationExample\ResourceEntitySeparationExample.csproj", "{F4097194-9415-418A-AB4E-315C5D5466AF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResourceEntitySeparationExampleTests", "test\ResourceEntitySeparationExampleTests\ResourceEntitySeparationExampleTests.csproj", "{778C4EB9-BD65-4C0F-9230-B5CB1D72186A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ResourceEntitySeparationExampleTests", "test\ResourceEntitySeparationExampleTests\ResourceEntitySeparationExampleTests.csproj", "{6DFA30D7-1679-4333-9779-6FB678E48EF5}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscoveryTests", "test\DiscoveryTests\DiscoveryTests.csproj", "{03032A2F-664D-4DD8-A82F-AD8A482EDD85}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GettingStarted", "src\Examples\GettingStarted\GettingStarted.csproj", "{DF9BFD82-D937-4907-B0B4-64670417115F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GettingStarted", "src\Examples\GettingStarted\GettingStarted.csproj", "{92BFF50F-BF96-43AD-AB86-A8B861C32412}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscoveryTests", "test\DiscoveryTests\DiscoveryTests.csproj", "{09C0C8D8-B721-4955-8889-55CB149C3B5C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "benchmarks\Benchmarks.csproj", "{DF0FCFB2-CB12-44BA-BBB5-1BE0BCFCD14C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -59,181 +59,181 @@ Global 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 + {9D36BE59-7C14-448B-984D-93A0E7816314}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9D36BE59-7C14-448B-984D-93A0E7816314}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9D36BE59-7C14-448B-984D-93A0E7816314}.Debug|x64.ActiveCfg = Debug|Any CPU + {9D36BE59-7C14-448B-984D-93A0E7816314}.Debug|x86.ActiveCfg = Debug|Any CPU + {9D36BE59-7C14-448B-984D-93A0E7816314}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9D36BE59-7C14-448B-984D-93A0E7816314}.Release|Any CPU.Build.0 = Release|Any CPU + {9D36BE59-7C14-448B-984D-93A0E7816314}.Release|x64.ActiveCfg = Release|Any CPU + {9D36BE59-7C14-448B-984D-93A0E7816314}.Release|x86.ActiveCfg = Release|Any CPU + {27FA7CD3-8DCD-4104-9AB4-B2D927F421B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27FA7CD3-8DCD-4104-9AB4-B2D927F421B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27FA7CD3-8DCD-4104-9AB4-B2D927F421B5}.Debug|x64.ActiveCfg = Debug|Any CPU + {27FA7CD3-8DCD-4104-9AB4-B2D927F421B5}.Debug|x86.ActiveCfg = Debug|Any CPU + {27FA7CD3-8DCD-4104-9AB4-B2D927F421B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27FA7CD3-8DCD-4104-9AB4-B2D927F421B5}.Release|Any CPU.Build.0 = Release|Any CPU + {27FA7CD3-8DCD-4104-9AB4-B2D927F421B5}.Release|x64.ActiveCfg = Release|Any CPU + {27FA7CD3-8DCD-4104-9AB4-B2D927F421B5}.Release|x86.ActiveCfg = Release|Any CPU + {99BAF03C-362B-41FA-9FFF-67F697EFC28C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99BAF03C-362B-41FA-9FFF-67F697EFC28C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99BAF03C-362B-41FA-9FFF-67F697EFC28C}.Debug|x64.ActiveCfg = Debug|Any CPU + {99BAF03C-362B-41FA-9FFF-67F697EFC28C}.Debug|x64.Build.0 = Debug|Any CPU + {99BAF03C-362B-41FA-9FFF-67F697EFC28C}.Debug|x86.ActiveCfg = Debug|Any CPU + {99BAF03C-362B-41FA-9FFF-67F697EFC28C}.Debug|x86.Build.0 = Debug|Any CPU + {99BAF03C-362B-41FA-9FFF-67F697EFC28C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99BAF03C-362B-41FA-9FFF-67F697EFC28C}.Release|Any CPU.Build.0 = Release|Any CPU + {99BAF03C-362B-41FA-9FFF-67F697EFC28C}.Release|x64.ActiveCfg = Release|Any CPU + {99BAF03C-362B-41FA-9FFF-67F697EFC28C}.Release|x64.Build.0 = Release|Any CPU + {99BAF03C-362B-41FA-9FFF-67F697EFC28C}.Release|x86.ActiveCfg = Release|Any CPU + {99BAF03C-362B-41FA-9FFF-67F697EFC28C}.Release|x86.Build.0 = Release|Any CPU + {1CC0831C-ED1D-442E-8421-331D50BD41F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1CC0831C-ED1D-442E-8421-331D50BD41F1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1CC0831C-ED1D-442E-8421-331D50BD41F1}.Debug|x64.ActiveCfg = Debug|Any CPU + {1CC0831C-ED1D-442E-8421-331D50BD41F1}.Debug|x64.Build.0 = Debug|Any CPU + {1CC0831C-ED1D-442E-8421-331D50BD41F1}.Debug|x86.ActiveCfg = Debug|Any CPU + {1CC0831C-ED1D-442E-8421-331D50BD41F1}.Debug|x86.Build.0 = Debug|Any CPU + {1CC0831C-ED1D-442E-8421-331D50BD41F1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1CC0831C-ED1D-442E-8421-331D50BD41F1}.Release|Any CPU.Build.0 = Release|Any CPU + {1CC0831C-ED1D-442E-8421-331D50BD41F1}.Release|x64.ActiveCfg = Release|Any CPU + {1CC0831C-ED1D-442E-8421-331D50BD41F1}.Release|x64.Build.0 = Release|Any CPU + {1CC0831C-ED1D-442E-8421-331D50BD41F1}.Release|x86.ActiveCfg = Release|Any CPU + {1CC0831C-ED1D-442E-8421-331D50BD41F1}.Release|x86.Build.0 = Release|Any CPU + {3AB43764-C57A-4B75-8C03-C671D3925BF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AB43764-C57A-4B75-8C03-C671D3925BF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AB43764-C57A-4B75-8C03-C671D3925BF3}.Debug|x64.ActiveCfg = Debug|Any CPU + {3AB43764-C57A-4B75-8C03-C671D3925BF3}.Debug|x64.Build.0 = Debug|Any CPU + {3AB43764-C57A-4B75-8C03-C671D3925BF3}.Debug|x86.ActiveCfg = Debug|Any CPU + {3AB43764-C57A-4B75-8C03-C671D3925BF3}.Debug|x86.Build.0 = Debug|Any CPU + {3AB43764-C57A-4B75-8C03-C671D3925BF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AB43764-C57A-4B75-8C03-C671D3925BF3}.Release|Any CPU.Build.0 = Release|Any CPU + {3AB43764-C57A-4B75-8C03-C671D3925BF3}.Release|x64.ActiveCfg = Release|Any CPU + {3AB43764-C57A-4B75-8C03-C671D3925BF3}.Release|x64.Build.0 = Release|Any CPU + {3AB43764-C57A-4B75-8C03-C671D3925BF3}.Release|x86.ActiveCfg = Release|Any CPU + {3AB43764-C57A-4B75-8C03-C671D3925BF3}.Release|x86.Build.0 = Release|Any CPU + {623792C0-5B7D-4D7D-A276-73F908FD4C34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {623792C0-5B7D-4D7D-A276-73F908FD4C34}.Debug|Any CPU.Build.0 = Debug|Any CPU + {623792C0-5B7D-4D7D-A276-73F908FD4C34}.Debug|x64.ActiveCfg = Debug|Any CPU + {623792C0-5B7D-4D7D-A276-73F908FD4C34}.Debug|x64.Build.0 = Debug|Any CPU + {623792C0-5B7D-4D7D-A276-73F908FD4C34}.Debug|x86.ActiveCfg = Debug|Any CPU + {623792C0-5B7D-4D7D-A276-73F908FD4C34}.Debug|x86.Build.0 = Debug|Any CPU + {623792C0-5B7D-4D7D-A276-73F908FD4C34}.Release|Any CPU.ActiveCfg = Release|Any CPU + {623792C0-5B7D-4D7D-A276-73F908FD4C34}.Release|Any CPU.Build.0 = Release|Any CPU + {623792C0-5B7D-4D7D-A276-73F908FD4C34}.Release|x64.ActiveCfg = Release|Any CPU + {623792C0-5B7D-4D7D-A276-73F908FD4C34}.Release|x64.Build.0 = Release|Any CPU + {623792C0-5B7D-4D7D-A276-73F908FD4C34}.Release|x86.ActiveCfg = Release|Any CPU + {623792C0-5B7D-4D7D-A276-73F908FD4C34}.Release|x86.Build.0 = Release|Any CPU + {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 + {65BF5960-3D9B-4230-99F4-A12CAA130792}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65BF5960-3D9B-4230-99F4-A12CAA130792}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65BF5960-3D9B-4230-99F4-A12CAA130792}.Debug|x64.ActiveCfg = Debug|Any CPU + {65BF5960-3D9B-4230-99F4-A12CAA130792}.Debug|x64.Build.0 = Debug|Any CPU + {65BF5960-3D9B-4230-99F4-A12CAA130792}.Debug|x86.ActiveCfg = Debug|Any CPU + {65BF5960-3D9B-4230-99F4-A12CAA130792}.Debug|x86.Build.0 = Debug|Any CPU + {65BF5960-3D9B-4230-99F4-A12CAA130792}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65BF5960-3D9B-4230-99F4-A12CAA130792}.Release|Any CPU.Build.0 = Release|Any CPU + {65BF5960-3D9B-4230-99F4-A12CAA130792}.Release|x64.ActiveCfg = Release|Any CPU + {65BF5960-3D9B-4230-99F4-A12CAA130792}.Release|x64.Build.0 = Release|Any CPU + {65BF5960-3D9B-4230-99F4-A12CAA130792}.Release|x86.ActiveCfg = Release|Any CPU + {65BF5960-3D9B-4230-99F4-A12CAA130792}.Release|x86.Build.0 = Release|Any CPU + {778C4EB9-BD65-4C0F-9230-B5CB1D72186A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {778C4EB9-BD65-4C0F-9230-B5CB1D72186A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {778C4EB9-BD65-4C0F-9230-B5CB1D72186A}.Debug|x64.ActiveCfg = Debug|Any CPU + {778C4EB9-BD65-4C0F-9230-B5CB1D72186A}.Debug|x64.Build.0 = Debug|Any CPU + {778C4EB9-BD65-4C0F-9230-B5CB1D72186A}.Debug|x86.ActiveCfg = Debug|Any CPU + {778C4EB9-BD65-4C0F-9230-B5CB1D72186A}.Debug|x86.Build.0 = Debug|Any CPU + {778C4EB9-BD65-4C0F-9230-B5CB1D72186A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {778C4EB9-BD65-4C0F-9230-B5CB1D72186A}.Release|Any CPU.Build.0 = Release|Any CPU + {778C4EB9-BD65-4C0F-9230-B5CB1D72186A}.Release|x64.ActiveCfg = Release|Any CPU + {778C4EB9-BD65-4C0F-9230-B5CB1D72186A}.Release|x64.Build.0 = Release|Any CPU + {778C4EB9-BD65-4C0F-9230-B5CB1D72186A}.Release|x86.ActiveCfg = Release|Any CPU + {778C4EB9-BD65-4C0F-9230-B5CB1D72186A}.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 + {92BFF50F-BF96-43AD-AB86-A8B861C32412}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92BFF50F-BF96-43AD-AB86-A8B861C32412}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92BFF50F-BF96-43AD-AB86-A8B861C32412}.Debug|x64.ActiveCfg = Debug|Any CPU + {92BFF50F-BF96-43AD-AB86-A8B861C32412}.Debug|x64.Build.0 = Debug|Any CPU + {92BFF50F-BF96-43AD-AB86-A8B861C32412}.Debug|x86.ActiveCfg = Debug|Any CPU + {92BFF50F-BF96-43AD-AB86-A8B861C32412}.Debug|x86.Build.0 = Debug|Any CPU + {92BFF50F-BF96-43AD-AB86-A8B861C32412}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92BFF50F-BF96-43AD-AB86-A8B861C32412}.Release|Any CPU.Build.0 = Release|Any CPU + {92BFF50F-BF96-43AD-AB86-A8B861C32412}.Release|x64.ActiveCfg = Release|Any CPU + {92BFF50F-BF96-43AD-AB86-A8B861C32412}.Release|x64.Build.0 = Release|Any CPU + {92BFF50F-BF96-43AD-AB86-A8B861C32412}.Release|x86.ActiveCfg = Release|Any CPU + {92BFF50F-BF96-43AD-AB86-A8B861C32412}.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|Any CPU.Build.0 = 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 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} + {9D36BE59-7C14-448B-984D-93A0E7816314} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF} + {27FA7CD3-8DCD-4104-9AB4-B2D927F421B5} = {026FBC6C-AF76-4568-9B87-EC73457899FD} + {99BAF03C-362B-41FA-9FFF-67F697EFC28C} = {026FBC6C-AF76-4568-9B87-EC73457899FD} + {1CC0831C-ED1D-442E-8421-331D50BD41F1} = {026FBC6C-AF76-4568-9B87-EC73457899FD} + {3AB43764-C57A-4B75-8C03-C671D3925BF3} = {026FBC6C-AF76-4568-9B87-EC73457899FD} + {623792C0-5B7D-4D7D-A276-73F908FD4C34} = {026FBC6C-AF76-4568-9B87-EC73457899FD} + {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} + {65BF5960-3D9B-4230-99F4-A12CAA130792} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} + {778C4EB9-BD65-4C0F-9230-B5CB1D72186A} = {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A2421882-8F0A-4905-928F-B550B192F9A4} diff --git a/README.md b/README.md index d0a5bb2cf2..3085181b1e 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; } } @@ -91,7 +91,7 @@ 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 ``` And then to run the tests: 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..d3e5e9df5b 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 ContextEntity { Attributes = new List { new AttrAttribute(ATTRIBUTE, ATTRIBUTE) } }); var options = new JsonApiOptions(); - _queryParser = new BenchmarkFacade(controllerContextMock.Object, options); + _queryParser = new BenchmarkFacade(requestMock.Object, options); } [Benchmark] @@ -58,8 +59,8 @@ private void Run(int iterations, Action action) { // this facade allows us to expose and micro-benchmark protected methods private class BenchmarkFacade : QueryParser { public BenchmarkFacade( - IControllerContext controllerContext, - JsonApiOptions options) : base(controllerContext, options) { } + IRequestContext requestManager, + JsonApiOptions options) : base(requestManager, options) { } public void _ParseSortParameters(string value) => base.ParseSortParameters(value); } diff --git a/benchmarks/Serialization/JsonApiDeserializer_Benchmarks.cs b/benchmarks/Serialization/JsonApiDeserializer_Benchmarks.cs index 983bc07f90..4214f447ba 100644 --- a/benchmarks/Serialization/JsonApiDeserializer_Benchmarks.cs +++ b/benchmarks/Serialization/JsonApiDeserializer_Benchmarks.cs @@ -4,17 +4,20 @@ 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 +32,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 requestManagerMock = new Mock(); + + requestManagerMock.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, requestManagerMock.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..4ec1ef99c4 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 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; } +// } +// } +//} diff --git a/docs/obsoletes.md b/docs/obsoletes.md new file mode 100644 index 0000000000..31a60249f2 --- /dev/null +++ b/docs/obsoletes.md @@ -0,0 +1,3 @@ +# For v5 + +* Anything to do with JsonApiContext, make it internal and fix anything related to it. \ No newline at end of file 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..2d1567659b 100644 --- a/src/Examples/GettingStarted/Controllers/ArticlesController.cs +++ b/src/Examples/GettingStarted/Controllers/ArticlesController.cs @@ -1,5 +1,7 @@ using GettingStarted.Models; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; namespace GettingStarted @@ -7,9 +9,10 @@ namespace GettingStarted public class ArticlesController : JsonApiController
{ public ArticlesController( - IJsonApiContext jsonApiContext, - IResourceService
resourceService) - : base(jsonApiContext, resourceService) + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, + IResourceService
resourceService) + : base(jsonApiOptions, resourceGraph, 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..8d8ee58a2a 100644 --- a/src/Examples/GettingStarted/Controllers/PeopleController.cs +++ b/src/Examples/GettingStarted/Controllers/PeopleController.cs @@ -1,5 +1,7 @@ using GettingStarted.Models; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; namespace GettingStarted @@ -7,9 +9,10 @@ namespace GettingStarted public class PeopleController : JsonApiController { public PeopleController( - IJsonApiContext jsonApiContext, - IResourceService resourceService) - : base(jsonApiContext, resourceService) + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, + IResourceService resourceService) + : base(jsonApiOptions, resourceGraph, resourceService) { } } -} \ No newline at end of file +} diff --git a/src/Examples/GettingStarted/GettingStarted.csproj b/src/Examples/GettingStarted/GettingStarted.csproj index ece976d5e5..e29e94ce6a 100644 --- a/src/Examples/GettingStarted/GettingStarted.csproj +++ b/src/Examples/GettingStarted/GettingStarted.csproj @@ -14,9 +14,9 @@ - - - + + + 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/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..d31458250c 100644 --- a/src/Examples/GettingStarted/ResourceDefinitionExample/ModelDefinition.cs +++ b/src/Examples/GettingStarted/ResourceDefinitionExample/ModelDefinition.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; namespace GettingStarted.ResourceDefinitionExample @@ -18,4 +18,4 @@ public ModelDefinition(IResourceGraph graph) : base(graph) 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..1c066c28c9 100644 --- a/src/Examples/GettingStarted/ResourceDefinitionExample/ModelsController.cs +++ b/src/Examples/GettingStarted/ResourceDefinitionExample/ModelsController.cs @@ -1,5 +1,6 @@ -using GettingStarted.Models; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; namespace GettingStarted.ResourceDefinitionExample @@ -7,9 +8,10 @@ namespace GettingStarted.ResourceDefinitionExample public class ModelsController : JsonApiController { public ModelsController( - IJsonApiContext jsonApiContext, - IResourceService resourceService) - : base(jsonApiContext, resourceService) + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, + IResourceService resourceService) + : base(jsonApiOptions, resourceGraph, resourceService) { } } -} \ No newline at end of file +} diff --git a/src/Examples/GettingStarted/Startup.cs b/src/Examples/GettingStarted/Startup.cs index 5d0fa8dc91..d5c805282b 100644 --- a/src/Examples/GettingStarted/Startup.cs +++ b/src/Examples/GettingStarted/Startup.cs @@ -20,11 +20,10 @@ 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); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, SampleDbContext context) diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs index 95aa7d69f9..abf6ce50ea 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs @@ -1,4 +1,6 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; @@ -7,9 +9,10 @@ namespace JsonApiDotNetCoreExample.Controllers public class ArticlesController : JsonApiController
{ public ArticlesController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService
resourceService) - : base(jsonApiContext, resourceService) + : base(jsonApiOptions, resourceGraph, 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..c46a9aa094 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs @@ -1,4 +1,6 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Mvc; @@ -11,10 +13,11 @@ namespace JsonApiDotNetCoreExample.Controllers public class CamelCasedModelsController : JsonApiController { public CamelCasedModelsController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs index 28a47eb419..5c6cab290d 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs @@ -8,8 +8,8 @@ public class PassportsController : JsonApiController { public PassportsController( IJsonApiContext jsonApiContext, - IResourceService resourceService) + IResourceService resourceService) : base(jsonApiContext, resourceService) { } } -} \ No newline at end of file +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs index e249e2af53..d29cafe508 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs @@ -1,4 +1,6 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.Extensions.Logging; @@ -8,10 +10,11 @@ namespace JsonApiDotNetCoreExample.Controllers public class PeopleController : JsonApiController { public PeopleController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/PersonRolesController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/PersonRolesController.cs index dbc3b482f5..0234eb6899 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/PersonRolesController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/PersonRolesController.cs @@ -1,4 +1,6 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.Extensions.Logging; @@ -8,10 +10,11 @@ namespace JsonApiDotNetCoreExample.Controllers public class PersonRolesController : JsonApiController { public PersonRolesController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs index 6a27038191..56bc6fda48 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,17 @@ 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, + IResourceGraph resourceGraph, + IDbContextResolver contextResolver, + IResourceService resourceService, + ILoggerFactory loggerFactory) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) { _dbResolver = contextResolver; - } [HttpPatch("{id}")] @@ -40,4 +41,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..c7f3a6244e 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs @@ -1,4 +1,6 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.Extensions.Logging; @@ -8,10 +10,11 @@ namespace JsonApiDotNetCoreExample.Controllers public class TodoItemsController : JsonApiController { public TodoItemsController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOPtions, + IResourceGraph resourceGraph, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOPtions, resourceGraph, resourceService, loggerFactory) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs index ca2e860fa9..17b11215c4 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs @@ -9,8 +9,7 @@ namespace JsonApiDotNetCoreExample.Controllers { - [DisableRoutingConvention] - [Route("custom/route/todo-items")] + [DisableRoutingConvention, Route("custom/route/todo-items")] public class TodoItemsCustomController : CustomJsonApiController { public TodoItemsCustomController( diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs index 9bab3cf544..18e566dc07 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs @@ -1,4 +1,6 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; @@ -11,10 +13,11 @@ public abstract class AbstractTodoItemsController : JsonApiController where T : class, IIdentifiable { protected AbstractTodoItemsController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService service, ILoggerFactory loggerFactory) - : base(jsonApiContext, service, loggerFactory) + : base(jsonApiOptions, resourceGraph, service, loggerFactory) { } } @@ -22,10 +25,11 @@ protected AbstractTodoItemsController( public class TodoItemsTestController : AbstractTodoItemsController { public TodoItemsTestController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService service, ILoggerFactory loggerFactory) - : base(jsonApiContext, service, loggerFactory) + : base(jsonApiOptions, resourceGraph, service, loggerFactory) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs index dbd144caa4..475b93b300 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs @@ -1,4 +1,6 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.Extensions.Logging; @@ -8,10 +10,11 @@ namespace JsonApiDotNetCoreExample.Controllers public class UsersController : JsonApiController { public UsersController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs b/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs index d7147123f6..6a6f7994fb 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs @@ -92,5 +92,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) 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..92f1bf4fa0 100644 --- a/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj +++ b/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj @@ -13,8 +13,8 @@ - - + + diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs index 3b3d44a8e2..9c88c3beeb 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.Links; using JsonApiDotNetCore.Services; namespace JsonApiDotNetCoreExample.Models @@ -11,7 +12,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 +46,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 +54,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/Resources/ArticleResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/ArticleResource.cs index 66429f175c..1e3230c759 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/ArticleResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/ArticleResource.cs @@ -5,8 +5,7 @@ using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Hooks; using JsonApiDotNetCoreExample.Models; -using Microsoft.Extensions.Logging; -using System.Security.Principal; +using JsonApiDotNetCore.Internal.Contracts; namespace JsonApiDotNetCoreExample.Resources { diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResource.cs index 7ad9659f18..96585a9458 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResource.cs @@ -1,8 +1,10 @@ -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 diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/PassportResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/PassportResource.cs index 457468c484..ba8b722277 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/PassportResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/PassportResource.cs @@ -5,6 +5,7 @@ using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Hooks; using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCore.Internal.Contracts; namespace JsonApiDotNetCoreExample.Resources { diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/PersonResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/PersonResource.cs index 4887414e73..82f6dac17f 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/PersonResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/PersonResource.cs @@ -1,12 +1,13 @@ 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) { } @@ -20,5 +21,14 @@ 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" } } + }; + } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/TagResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/TagResource.cs index e3b3100ddd..cd266303f3 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 graph) : base(graph) { } 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..1c9f89a7fc 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/TodoResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/TodoResource.cs @@ -4,6 +4,7 @@ using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Hooks; using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCore.Internal.Contracts; namespace JsonApiDotNetCoreExample.Resources { diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs index ec54b6144e..f6810f086b 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs @@ -3,16 +3,17 @@ 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) { } + public UserResource(IResourceGraph graph, IExposedFieldExplorer fieldExplorer) : base(fieldExplorer, graph) { } - protected override List OutputAttrs() - => Remove(user => user.Password); + //protected override List OutputAttrs() + // => Remove(user => user.Password); public override QueryFilters GetQueryFilters() { diff --git a/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs b/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs new file mode 100644 index 0000000000..83bb43fd19 --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs @@ -0,0 +1,39 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Hooks; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreExample.Models; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; + +namespace JsonApiDotNetCoreExample.Services +{ + public class CustomArticleService : EntityResourceService

+ { + public CustomArticleService( + IEntityRepository
repository, + IJsonApiOptions jsonApiOptions, + IRequestContext queryManager, + IPageQueryService pageManager, + IResourceGraph resourceGraph, + IResourceHookExecutor resourceHookExecutor = null, + ILoggerFactory loggerFactory = null + ) : base(repository: repository, jsonApiOptions, queryManager, pageManager, resourceGraph:resourceGraph, loggerFactory, resourceHookExecutor) + { } + + 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/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startup.cs index 44784f7eac..a784de13f6 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startup.cs @@ -29,9 +29,6 @@ public virtual IServiceProvider 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) @@ -40,9 +37,8 @@ public virtual IServiceProvider ConfigureServices(IServiceCollection services) options.DefaultPageSize = 5; options.IncludeTotalRecordCount = true; options.EnableResourceHooks = true; - options.LoadDatabaseValues = true; + options.LoaDatabaseValues = true; }, - mvcBuilder, discovery => discovery.AddCurrentAssembly()); return services.BuildServiceProvider(); @@ -55,9 +51,7 @@ public virtual void Configure( AppDbContext context) { context.Database.EnsureCreated(); - loggerFactory.AddConsole(Config.GetSection("Logging")); - app.UseJsonApi(); } diff --git a/src/Examples/NoEntityFrameworkExample/Controllers/CustomTodoItemsController.cs b/src/Examples/NoEntityFrameworkExample/Controllers/CustomTodoItemsController.cs index a6ded9749f..4e93d26bea 100644 --- a/src/Examples/NoEntityFrameworkExample/Controllers/CustomTodoItemsController.cs +++ b/src/Examples/NoEntityFrameworkExample/Controllers/CustomTodoItemsController.cs @@ -1,4 +1,6 @@ -using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.Extensions.Logging; @@ -8,10 +10,11 @@ namespace NoEntityFrameworkExample.Controllers public class CustomTodoItemsController : JsonApiController { public CustomTodoItemsController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) { } } } diff --git a/src/Examples/ReportsExample/Controllers/ReportsController.cs b/src/Examples/ReportsExample/Controllers/ReportsController.cs index 6f431d9291..e421477c20 100644 --- a/src/Examples/ReportsExample/Controllers/ReportsController.cs +++ b/src/Examples/ReportsExample/Controllers/ReportsController.cs @@ -1,17 +1,20 @@ -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, + IResourceGraph resourceGraph, IGetAllService getAll) - : base(jsonApiContext, getAll: getAll) + : base(jsonApiOptions, resourceGraph, getAll: getAll) { } [HttpGet] diff --git a/src/Examples/ReportsExample/Startup.cs b/src/Examples/ReportsExample/Startup.cs index b71b7fa74a..4f49e87db6 100644 --- a/src/Examples/ReportsExample/Startup.cs +++ b/src/Examples/ReportsExample/Startup.cs @@ -27,8 +27,7 @@ public virtual void ConfigureServices(IServiceCollection services) var mvcBuilder = services.AddMvcCore(); services.AddJsonApi( opt => opt.Namespace = "api", - mvcBuilder, - discovery => discovery.AddCurrentAssembly()); + discovery => discovery.AddCurrentAssembly(), mvcBuilder); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) diff --git a/src/Examples/ResourceEntitySeparationExample/Controllers/CoursesController.cs b/src/Examples/ResourceEntitySeparationExample/Controllers/CoursesController.cs index 6809ace0bb..48d280f5cb 100644 --- a/src/Examples/ResourceEntitySeparationExample/Controllers/CoursesController.cs +++ b/src/Examples/ResourceEntitySeparationExample/Controllers/CoursesController.cs @@ -1,4 +1,6 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models.Resources; using Microsoft.Extensions.Logging; @@ -8,10 +10,11 @@ namespace ResourceEntitySeparationExample.Controllers public class CoursesController : JsonApiController { public CoursesController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) { } } } diff --git a/src/Examples/ResourceEntitySeparationExample/Controllers/DepartmentsController.cs b/src/Examples/ResourceEntitySeparationExample/Controllers/DepartmentsController.cs index 08f3ab33ad..63310743ac 100644 --- a/src/Examples/ResourceEntitySeparationExample/Controllers/DepartmentsController.cs +++ b/src/Examples/ResourceEntitySeparationExample/Controllers/DepartmentsController.cs @@ -1,5 +1,7 @@ using System; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models.Resources; using Microsoft.Extensions.Logging; @@ -9,10 +11,11 @@ namespace ResourceEntitySeparationExample.Controllers public class DepartmentsController : JsonApiController { public DepartmentsController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) { } } } diff --git a/src/Examples/ResourceEntitySeparationExample/Controllers/StudentsController.cs b/src/Examples/ResourceEntitySeparationExample/Controllers/StudentsController.cs index 34d5d33031..5f3551849a 100644 --- a/src/Examples/ResourceEntitySeparationExample/Controllers/StudentsController.cs +++ b/src/Examples/ResourceEntitySeparationExample/Controllers/StudentsController.cs @@ -1,4 +1,6 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models.Resources; using Microsoft.Extensions.Logging; @@ -8,10 +10,11 @@ namespace ResourceEntitySeparationExample.Controllers public class StudentsController : JsonApiController { public StudentsController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) { } } } diff --git a/src/Examples/ResourceEntitySeparationExample/Startup.cs b/src/Examples/ResourceEntitySeparationExample/Startup.cs index a99febfee8..f87dea6935 100644 --- a/src/Examples/ResourceEntitySeparationExample/Startup.cs +++ b/src/Examples/ResourceEntitySeparationExample/Startup.cs @@ -43,18 +43,19 @@ public virtual IServiceProvider ConfigureServices(IServiceCollection services) ServiceLifetime.Transient); services.AddScoped>(); - var mvcBuilder = services.AddMvcCore(); services.AddJsonApi(options => { options.Namespace = "api/v1"; options.DefaultPageSize = 10; options.IncludeTotalRecordCount = true; - options.BuildResourceGraph((builder) => { + options.EnableResourceHooks = false; // not supported with ResourceEntitySeparation + options.BuildResourceGraph((builder) => + { builder.AddResource("courses"); builder.AddResource("departments"); builder.AddResource("students"); }); - }, mvcBuilder); + }); services.AddAutoMapper(); services.AddScoped(); diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index ad52aeb8bb..55f9357476 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -1,354 +1,372 @@ -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() - }; - } - } -} +//using System; +//using System.Collections; +//using System.Collections.Generic; +//using System.Linq; +//using JsonApiDotNetCore.Extensions; +//using JsonApiDotNetCore.Internal; +//using JsonApiDotNetCore.Internal.Contracts; +//using JsonApiDotNetCore.Managers.Contracts; +//using JsonApiDotNetCore.Models; +//using JsonApiDotNetCore.Services; + +//namespace JsonApiDotNetCore.Builders +//{ +// /// +// public class DocumentBuilder : IDocumentBuilder +// { +// private readonly IRequestManager _requestManager; +// private readonly IPageManager _pageManager; +// private readonly IJsonApiContext _jsonApiContext; +// private readonly IResourceGraph _resourceGraph; +// private readonly IRequestMeta _requestMeta; +// private readonly DocumentBuilderOptions _documentBuilderOptions; +// private readonly IScopedServiceProvider _scopedServiceProvider; + +// public DocumentBuilder( +// IJsonApiContext jsonApiContext, +// IPageManager pageManager, +// IRequestManager requestManager, +// IRequestMeta requestMeta = null, +// IDocumentBuilderOptionsProvider documentBuilderOptionsProvider = null, +// IScopedServiceProvider scopedServiceProvider = null) +// { +// _pageManager = pageManager; +// _jsonApiContext = jsonApiContext; +// _requestManager = requestManager ?? jsonApiContext.RequestManager; +// _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 = _pageManager.GetPageLinks(); +// } + +// 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 = _pageManager.GetPageLinks(); +// } + +// foreach (var entity in enumeratedEntities) +// { +// documents.Data.Add(GetData(contextEntity, entity, resourceDefinition)); +// documents.Included = AppendIncludedObject(documents.Included, contextEntity, entity); +// } + +// return documents; +// } + + +// private bool ShouldIncludePageLinks(ContextEntity entity) => entity.Links.HasFlag(Link.Paging); + +// private Dictionary GetMeta(IIdentifiable entity) +// { +// var builder = _jsonApiContext.MetaBuilder; +// if (_jsonApiContext.Options.IncludeTotalRecordCount && _pageManager.TotalRecords != null) +// builder.Add("total-records", _pageManager.TotalRecords); + +// if (_requestMeta != null) +// builder.Add(_requestMeta.GetMeta()); + +// if (entity != null && entity is IHasMeta metaEntity) +// builder.Add(metaEntity.GetMeta()); + +// var meta = builder.Build(); +// if (meta.Count > 0) +// return meta; + +// return null; +// } + + +// 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) +// && ((_requestManager.QuerySet == null +// || _requestManager.QuerySet.Fields.Count == 0) +// || _requestManager.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.Options, _requestManager); + +// var relationshipData = new RelationshipData(); + +// if (_jsonApiContext.Options.DefaultRelationshipLinks.HasFlag(Link.None) == false && attr.RelationshipLinks.HasFlag(Link.None) == false) +// { +// relationshipData.Links = new RelationshipLinks(); +// if (attr.RelationshipLinks.HasFlag(Link.Self)) +// { +// relationshipData.Links.Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.StringId, attr.PublicRelationshipName); +// } + +// if (attr.RelationshipLinks.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 (_requestManager.IncludedRelationships != null) +// { +// foreach (var relationshipName in _requestManager.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/DocumentBuilderOptionsProvider.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilderOptionsProvider.cs index 37c5d51273..ee929bf801 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilderOptionsProvider.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilderOptionsProvider.cs @@ -1,3 +1,4 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; @@ -5,26 +6,28 @@ namespace JsonApiDotNetCore.Builders { public class DocumentBuilderOptionsProvider : IDocumentBuilderOptionsProvider { - private readonly IJsonApiContext _jsonApiContext; - private readonly IHttpContextAccessor _httpContextAccessor; - - public DocumentBuilderOptionsProvider(IJsonApiContext jsonApiContext, IHttpContextAccessor httpContextAccessor) + public DocumentBuilderOptionsProvider(IJsonApiOptions options) { - _jsonApiContext = jsonApiContext; - _httpContextAccessor = httpContextAccessor; } - public DocumentBuilderOptions GetDocumentBuilderOptions() + public SerializerBehaviour 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 SerializerBehaviour(omitNullValuedAttributes); + return null; } } - return new DocumentBuilderOptions(this._jsonApiContext.Options.NullAttributeResponseBehavior.OmitNullValuedAttributes); + //return new SerializerBehaviour(this._jsonApiContext.Options.NullAttributeResponseBehavior.OmitNullValuedAttributes); + + return null; } } + + public interface IDocumentBuilderOptionsProvider + { + } } diff --git a/src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs index 45ba096447..1850d36604 100644 --- a/src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.Builders public interface IDocumentBuilder { /// - /// Builds a json:api document from the provided resource instance. + /// Builds a Json:Api document from the provided resource instance. /// /// The resource to convert. Document Build(IIdentifiable entity); @@ -17,7 +17,7 @@ public interface IDocumentBuilder /// Builds a json:api document from the provided resource instances. /// /// The collection of resources to convert. - Documents Build(IEnumerable entities); + //Documents Build(IEnumerable entities); [Obsolete("You should specify an IResourceDefinition implementation using the GetData/3 overload.")] ResourceObject GetData(ContextEntity contextEntity, IIdentifiable entity); @@ -32,6 +32,6 @@ public interface IDocumentBuilder /// 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); + ResourceObject GetData(ContextEntity contextEntity, IIdentifiable entity, object 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/IResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs new file mode 100644 index 0000000000..434875564a --- /dev/null +++ b/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +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.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; + + IResourceGraphBuilder AddControllerPairing(Type controller, Type model = null); + + /// + /// 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); + + + } +} 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 72% rename from src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs rename to src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs index dfa6b665a0..5422cea8a1 100644 --- a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs @@ -7,93 +7,64 @@ 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 Dictionary> _controllerMapper = new Dictionary>() { }; + private List _undefinedMapper = new List() { }; private bool _usesDbContext; - private IResourceNameFormatter _resourceNameFormatter = JsonApiOptions.ResourceNameFormatter; + private IResourceNameFormatter _resourceNameFormatter; - /// - public Link DocumentLinks { get; set; } = Link.All; + public ResourceGraphBuilder(IResourceNameFormatter formatter = null) + { + _resourceNameFormatter = formatter ?? new DefaultResourceNameFormatter(); + } /// 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); + + List controllerContexts = new List() { }; + foreach(var cm in _controllerMapper) + { + var model = cm.Key; + foreach (var controller in cm.Value) + { + var controllerName = controller.Name.Replace("Controller", ""); + + controllerContexts.Add(new ControllerResourceMap + { + Resource = model, + ControllerName = controllerName, + }); - var graph = new ResourceGraph(_entities, _usesDbContext, _validationResults); + } + } + var graph = new ResourceGraph(_entities, _usesDbContext, _validationResults, controllerContexts); return graph; } + private void SetResourceLinksOptions(ContextEntity resourceContext) + { + var attribute = (LinksAttribute)resourceContext.EntityType.GetCustomAttribute(typeof(LinksAttribute)); + if (attribute != null) + { + resourceContext.RelationshipLinks = attribute.RelationshipLinks; + resourceContext.ResourceLinks = attribute.ResourceLinks; + resourceContext.TopLevelLinks = attribute.TopLevelLinks; + } + } + /// public IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable => AddResource(pluralizedTypeName); @@ -124,14 +95,6 @@ public IResourceGraphBuilder AddResource(Type entityType, Type idType, string pl ResourceType = 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 +104,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 +123,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 +135,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; 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 +167,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}"); @@ -305,5 +270,23 @@ public IResourceGraphBuilder UseNameFormatter(IResourceNameFormatter resourceNam _resourceNameFormatter = resourceNameFormatter; return this; } + + public IResourceGraphBuilder AddControllerPairing(Type controller, Type model = null) + { + if (model == null) + { + _undefinedMapper.Add(controller); + return this; + } + if (_controllerMapper.Keys.Contains(model)) + { + _controllerMapper[model].Add(controller); + } + else + { + _controllerMapper.Add(model, new List() { controller }); + } + return this; + } } } diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilderOptions.cs b/src/JsonApiDotNetCore/Builders/SerializerOptions.cs similarity index 56% rename from src/JsonApiDotNetCore/Builders/DocumentBuilderOptions.cs rename to src/JsonApiDotNetCore/Builders/SerializerOptions.cs index a5b16bdd37..3d9e3f71fc 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilderOptions.cs +++ b/src/JsonApiDotNetCore/Builders/SerializerOptions.cs @@ -1,17 +1,21 @@ +using JsonApiDotNetCore.Configuration; + namespace JsonApiDotNetCore.Builders { /// /// Options used to configure how a model gets serialized into /// a json:api document. /// - public struct DocumentBuilderOptions + public class SerializerBehaviour { - /// - /// Do not serialize attributes with null values. - /// - public DocumentBuilderOptions(bool omitNullValuedAttributes = false) + /// Omit null values from attributes + public SerializerBehaviour(ISerializerOptions options) { - this.OmitNullValuedAttributes = omitNullValuedAttributes; + OmitNullValuedAttributes = options.NullAttributeResponseBehavior.OmitNullValuedAttributes; + if (options.NullAttributeResponseBehavior.AllowClientOverride) + { + + } } /// @@ -26,4 +30,10 @@ public DocumentBuilderOptions(bool omitNullValuedAttributes = false) /// public bool OmitNullValuedAttributes { get; private set; } } + + public interface ISerializerOptions + { + NullAttributeResponseBehavior NullAttributeResponseBehavior { get;set;} + } } + diff --git a/src/JsonApiDotNetCore/Configuration/IGlobalLinksConfiguration.cs b/src/JsonApiDotNetCore/Configuration/IGlobalLinksConfiguration.cs new file mode 100644 index 0000000000..57e47d7494 --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/IGlobalLinksConfiguration.cs @@ -0,0 +1,12 @@ +using JsonApiDotNetCore.Models.Links; + +namespace JsonApiDotNetCore.Configuration +{ + public interface IGlobalLinksConfiguration + { + bool RelativeLinks { get; } + Link RelationshipLinks { get; } + Link TopLevelLinks { get; } + Link ResourceLinks { get; } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs new file mode 100644 index 0000000000..cdc6a8a2e9 --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -0,0 +1,37 @@ +using JsonApiDotNetCore.Internal.Contracts; +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.Configuration +{ + public interface IJsonApiOptions : IGlobalLinksConfiguration, 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 EnableOperations { get; set; } + IResourceGraph ResourceGraph { get; set; } + bool AllowCustomQueryParameters { get; set; } + string Namespace { get; set; } + } + + public interface ISerializerOptions + { + NullAttributeResponseBehavior NullAttributeResponseBehavior { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 21efdb97ed..d45bc0f494 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -2,20 +2,85 @@ using System.Collections.Generic; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Graph; -using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.Links; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Configuration { + /// /// Global options /// - public class JsonApiOptions + public class JsonApiOptions : IJsonApiOptions { + /// + /// 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; } = false; + + /// + /// Configures globally which links to show in the + /// object for a requested resource. Setting can be overriden per resource by + /// setting the option or on the + /// RelationshipAttribute in the class definition of your model. + /// + /// + /// + /// options.DefaultRelationshipLinks = Link.None; + /// + /// + /// { + /// "type": "articles", + /// "id": "4309", + /// "relationships": { + /// "author": { "data": { "type": "people", "id": "1234" } + /// } + /// } + /// } + /// + /// + public Link RelationshipLinks { get; set; } = Link.All; + + + /// + /// Configures globally which links to show in the + /// object for a requested resource. Setting can be overriden per resource by + /// setting the option. + /// + public Link TopLevelLinks { get; set; } = Link.All; + + + /// + /// Configures globally which links to show in the + /// object for a requested resource. Setting can be overriden per resource by + /// setting the option. + /// + public Link ResourceLinks { get; set; } = Link.All; /// /// Provides an interface for formatting resource names by convention @@ -31,7 +96,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 +115,7 @@ public class JsonApiOptions /// /// Defaults to . /// - public bool LoadDatabaseValues { get; set; } = false; + public bool LoaDatabaseValues { get; set; } = false; /// /// The base URL Namespace @@ -92,52 +157,9 @@ public class JsonApiOptions /// /// The graph of all resources exposed by this application. /// + [Obsolete("Use the standalone resourcegraph")] 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. /// @@ -163,7 +185,7 @@ public class JsonApiOptions /// /// 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 + /// in subsequent releases. For now, ijt should be considered /// experimental. /// /// @@ -183,8 +205,7 @@ 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 diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index 93fd4826e2..cb9eb09510 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -1,41 +1,17 @@ + using System; using System.Collections.Generic; using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; 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 +24,26 @@ 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; + private readonly IResourceGraph _resourceGraph; + public BaseJsonApiController( - IJsonApiContext jsonApiContext, - IResourceService resourceService) + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraphManager, + IResourceService resourceService, + ILoggerFactory loggerFactory) { - _jsonApiContext = jsonApiContext.ApplyContext(this); + if(loggerFactory != null) + { + _logger = loggerFactory.CreateLogger>(); + } + else + { + _logger = new Logger>(new LoggerFactory()); + } + _jsonApiOptions = jsonApiOptions; + _resourceGraph = resourceGraphManager; _getAll = resourceService; _getById = resourceService; _getRelationship = resourceService; @@ -63,14 +52,20 @@ public BaseJsonApiController( _update = resourceService; _updateRelationships = resourceService; _delete = resourceService; + ParseQueryParams(); + } + + private void ParseQueryParams() + { + } 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 +76,22 @@ public BaseJsonApiController( _delete = cmdService; } + /// + /// Base constructor + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// public BaseJsonApiController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IGetAllService getAll = null, IGetByIdService getById = null, IGetRelationshipService getRelationship = null, @@ -92,7 +101,8 @@ public BaseJsonApiController( IUpdateRelationshipService updateRelationships = null, IDeleteService delete = null) { - _jsonApiContext = jsonApiContext.ApplyContext(this); + _resourceGraph = resourceGraph; + _jsonApiOptions = jsonApiOptions; _getAll = getAll; _getById = getById; _getRelationship = getRelationship; @@ -106,28 +116,27 @@ 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(); - + } return Ok(entity); } 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 +147,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 +159,13 @@ 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(ModelStateExtensions.ConvertToErrorCollection(ModelState, _resourceGraph)); + } entity = await _create.CreateAsync(entity); @@ -166,12 +175,13 @@ 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(ModelStateExtensions.ConvertToErrorCollection(ModelState, _resourceGraph)); + } var updatedEntity = await _update.UpdateAsync(id, entity); @@ -184,22 +194,46 @@ public virtual async Task PatchAsync(TId id, [FromBody] T entity) public virtual async Task PatchRelationshipsAsync(TId id, string relationshipName, [FromBody] List 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(); } } + 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, + IResourceGraph resourceGraph, + 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, resourceGraph, getAll, getById, getRelationship, getRelationships, create, update, updateRelationships, delete) { } + } } diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs index 16ab4aa74a..ebcad6bd13 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs @@ -1,18 +1,19 @@ 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 +21,9 @@ public class JsonApiCmdController : BaseJsonApiController where T : class, IIdentifiable { public JsonApiCmdController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, IResourceService resourceService) - : base(jsonApiContext, resourceService) + : base(jsonApiOptions, resourceService) { } [HttpPost] diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs index a77c03da06..91d78f3ac2 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs @@ -1,5 +1,8 @@ +using System; using System.Collections.Generic; using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc; @@ -7,53 +10,28 @@ namespace JsonApiDotNetCore.Controllers { - public class JsonApiController - : JsonApiController 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 class JsonApiController : BaseJsonApiController where T : class, IIdentifiable { + /// + /// + /// + /// + /// + /// + /// public JsonApiController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService resourceService, - ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService) - { } - - public JsonApiController( - IJsonApiContext jsonApiContext, - IResourceService resourceService) - : base(jsonApiContext, resourceService) + ILoggerFactory loggerFactory = null) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory = null) { } public JsonApiController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IGetAllService getAll = null, IGetByIdService getById = null, IGetRelationshipService getRelationship = null, @@ -62,7 +40,7 @@ public JsonApiController( IUpdateService update = null, IUpdateRelationshipService updateRelationships = null, IDeleteService delete = null - ) : base(jsonApiContext, getAll, getById, getRelationship, getRelationships, create, update, updateRelationships, delete) { } + ) : base(jsonApiOptions, resourceGraph, getAll, getById, getRelationship, getRelationships, create, update, updateRelationships, delete) { } [HttpGet] public override async Task GetAsync() => await base.GetAsync(); @@ -84,7 +62,9 @@ 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( @@ -94,4 +74,42 @@ public override async Task PatchRelationshipsAsync( [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, + IResourceGraph resourceGraph, + IResourceService resourceService, + ILoggerFactory loggerFactory = null + ) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) + { } + + public JsonApiController( + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, + 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, resourceGraph, getAll, getById, getRelationship, getRelationships, create, update, updateRelationships, delete) { } + + + } } diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs index 5211e5fa3b..9642efbcee 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,9 +10,9 @@ public class JsonApiQueryController : JsonApiQueryController where T : class, IIdentifiable { public JsonApiQueryController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, IResourceService resourceService) - : base(jsonApiContext, resourceService) + : base(jsonApiOptions, resourceService) { } } @@ -19,9 +20,9 @@ public class JsonApiQueryController : BaseJsonApiController where T : class, IIdentifiable { public JsonApiQueryController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, IResourceService resourceService) - : base(jsonApiContext, resourceService) + : base(jsonApiOptions, resourceService) { } [HttpGet] 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/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 889e2f7198..d24746419e 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -5,36 +5,17 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization; 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. @@ -44,41 +25,43 @@ public class DefaultEntityRepository IEntityFrameworkRepository where TEntity : class, IIdentifiable { + private readonly IRequestContext _requestManager; + private readonly IUpdatedFields _updatedFields; private readonly DbContext _context; private readonly DbSet _dbSet; private readonly ILogger _logger; - private readonly IJsonApiContext _jsonApiContext; + private readonly IResourceGraph _resourceGraph; private readonly IGenericProcessorFactory _genericProcessorFactory; private readonly ResourceDefinition _resourceDefinition; + public DefaultEntityRepository( - IJsonApiContext jsonApiContext, + IUpdatedFields updatedFields, IDbContextResolver contextResolver, + IResourceGraph resourceGraph, + IGenericProcessorFactory genericProcessorFactory, ResourceDefinition resourceDefinition = null) - { - _context = contextResolver.GetContext(); - _dbSet = _context.Set(); - _jsonApiContext = jsonApiContext; - _genericProcessorFactory = _jsonApiContext.GenericProcessorFactory; - _resourceDefinition = resourceDefinition; - } + : this(null, updatedFields, contextResolver, resourceGraph, genericProcessorFactory, resourceDefinition) + { } public DefaultEntityRepository( ILoggerFactory loggerFactory, - IJsonApiContext jsonApiContext, + IUpdatedFields updatedFields, IDbContextResolver contextResolver, + IResourceGraph resourceGraph, + IGenericProcessorFactory genericProcessorFactory, ResourceDefinition resourceDefinition = null) { + _logger = loggerFactory.CreateLogger>(); + _updatedFields = updatedFields; + _resourceGraph = resourceGraph; + _genericProcessorFactory = genericProcessorFactory; _context = contextResolver.GetContext(); _dbSet = _context.Set(); - _jsonApiContext = jsonApiContext; - _logger = loggerFactory.CreateLogger>(); - _genericProcessorFactory = _jsonApiContext.GenericProcessorFactory; _resourceDefinition = resourceDefinition; } /// - public virtual IQueryable Get() - => _dbSet; + public virtual IQueryable Get() => _dbSet; /// public virtual IQueryable Select(IQueryable entities, List fields) @@ -100,7 +83,7 @@ public virtual IQueryable Filter(IQueryable entities, FilterQu return defaultQueryFilter(entities, filterQuery); } } - return entities.Filter(_jsonApiContext, filterQuery); + return entities.Filter(new AttrFilterQuery(_requestManager, _resourceGraph, filterQuery)); } /// @@ -127,24 +110,22 @@ public virtual IQueryable Sort(IQueryable entities, List public virtual async Task GetAsync(TId id) { - return await Select(Get(), _jsonApiContext.QuerySet?.Fields).SingleOrDefaultAsync(e => e.Id.Equals(id)); + return await Select(Get(), _requestManager.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 includedSet = Include(Select(Get(), _requestManager.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) + foreach (var relationshipAttr in _updatedFields.Relationships) { var trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, entity, out bool wasAlreadyTracked); LoadInverseRelationships(trackedRelationshipValue, relationshipAttr); @@ -204,15 +185,15 @@ private void LoadInverseRelationships(object trackedRelationshipValue, Relations } } - private bool IsHasOneRelationship(string internalRelationshipName, Type type) { - var relationshipAttr = _jsonApiContext.ResourceGraph.GetContextEntity(type).Relationships.SingleOrDefault(r => r.InternalRelationshipName == internalRelationshipName); - if(relationshipAttr != null) + var relationshipAttr = _resourceGraph.GetContextEntity(type).Relationships.SingleOrDefault(r => r.InternalRelationshipName == internalRelationshipName); + if (relationshipAttr != null) { if (relationshipAttr is HasOneAttribute) return true; return false; - } else + } + 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. @@ -225,14 +206,13 @@ private bool IsHasOneRelationship(string internalRelationshipName, Type type) public void DetachRelationshipPointers(TEntity entity) { - foreach (var relationshipAttr in _jsonApiContext.RelationshipsToUpdate.Keys) + foreach (var relationshipAttr in _updatedFields.Relationships) { 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 { @@ -268,10 +248,10 @@ public virtual async Task UpdateAsync(TEntity updatedEntity) if (databaseEntity == null) return null; - foreach (var attr in _jsonApiContext.AttributesToUpdate.Keys) + foreach (var attr in _updatedFields.Attributes) attr.SetValue(databaseEntity, attr.GetValue(updatedEntity)); - foreach (var relationshipAttr in _jsonApiContext.RelationshipsToUpdate?.Keys) + foreach (var relationshipAttr in _updatedFields.Relationships) { /// loads databasePerson.todoItems LoadCurrentRelationships(databaseEntity, relationshipAttr); @@ -330,7 +310,7 @@ private IList GetTrackedManyRelationshipValue(IEnumerable relatio /// 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; + Type entityType = entityResourceSeparation ? null : relationshipAttr.DependentType; var trackedPointerCollection = relationshipValueList.Select(pointer => { /// todo: we can't just use relationshipAttr.DependentType because @@ -390,28 +370,18 @@ public virtual IQueryable Include(IQueryable entities, string // variables mutated in recursive loop // TODO: make recursive method string internalRelationshipPath = null; - var entity = _jsonApiContext.RequestEntity; + var entity = _requestManager.GetRequestResource(); 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); + entity = _resourceGraph.GetContextEntity(relationship.Type); } return entities.Include(internalRelationshipPath); @@ -433,7 +403,7 @@ public virtual async Task> PageAsync(IQueryable en // 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)); @@ -463,7 +433,6 @@ public async Task> ToListAsync(IQueryable entiti : entities.ToList(); } - /// /// Before assigning new relationship values (UpdateAsync), we need to /// attach the current database values of the relationship to the dbcontext, else @@ -581,4 +550,28 @@ private IIdentifiable AttachOrGetTracked(IIdentifiable relationshipValue) return null; } } + + /// + public class DefaultEntityRepository + : DefaultEntityRepository, + IEntityRepository + where TEntity : class, IIdentifiable + { + //public DefaultEntityRepository( + //IUpdatedFields updatedFields, + //IDbContextResolver contextResolver, + //IResourceGraph resourceGraph, + //IGenericProcessorFactory genericProcessorFactory, + //ResourceDefinition resourceDefinition = null) : base (updatedFields CO) + //{ } + + //public DefaultEntityRepository( + // IUpdatedFields updatedFields, + // ILoggerFactory loggerFactory, + // IDbContextResolver contextResolver, + // IResourceGraph resourceGraph, + // IGenericProcessorFactory genericProcessorFactory, + // ResourceDefinition resourceDefinition = null) + //{ } + } } diff --git a/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs b/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs index e66b51a7a0..44c741043c 100644 --- a/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs @@ -1,11 +1,14 @@ using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Middleware; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Routing; namespace JsonApiDotNetCore.Extensions { @@ -17,10 +20,14 @@ public static IApplicationBuilder UseJsonApi(this IApplicationBuilder app, bool DisableDetailedErrorsIfProduction(app); LogResourceGraphValidations(app); + app.UseEndpointRouting(); + app.UseMiddleware(); if (useMvc) + { app.UseMvc(); + } using (var scope = app.ApplicationServices.CreateScope()) { diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 87de9d187d..b6b41fd0ca 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -113,17 +113,7 @@ 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) { diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index cb1e635c7a..496c50827e 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -9,9 +9,13 @@ 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.Serialization.Contracts; + using JsonApiDotNetCore.Hooks; using JsonApiDotNetCore.Services; using JsonApiDotNetCore.Services.Operations; @@ -20,67 +24,115 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using JsonApiDotNetCore.Serialization.Contracts; +using JsonApiDotNetCore.Internal.Contracts; namespace JsonApiDotNetCore.Extensions { // ReSharper disable once InconsistentNaming public static class IServiceCollectionExtensions { - public static IServiceCollection AddJsonApi(this IServiceCollection services) + static private readonly Action _noopConfig = opt => { }; + static private JsonApiOptions _options { get { return new JsonApiOptions(); } } + public static IServiceCollection AddJsonApi(this IServiceCollection services, + IMvcCoreBuilder mvcBuilder = null) where TContext : DbContext { - var mvcBuilder = services.AddMvcCore(); - return AddJsonApi(services, opt => { }, mvcBuilder); + return AddJsonApi(services, _noopConfig, mvcBuilder); } - public static IServiceCollection AddJsonApi(this IServiceCollection services, Action options) + /// + /// Enabling JsonApiDotNetCore using the EF Core DbContext to build the ResourceGraph. + /// + /// + /// + /// + /// + public static IServiceCollection AddJsonApi(this IServiceCollection services, + Action configureAction, + IMvcCoreBuilder mvcBuilder = null) where TContext : DbContext { - var mvcBuilder = services.AddMvcCore(); - return AddJsonApi(services, options, mvcBuilder); + var options = _options; + // add basic Mvc functionality + mvcBuilder = mvcBuilder ?? services.AddMvcCore(); + // set standard options + configureAction(options); + + // ResourceGraphBuilder should not be exposed on JsonApiOptions. + // Instead, ResourceGraphBuilder should consume JsonApiOptions + + // build the resource graph using ef core DbContext + options.BuildResourceGraph(builder => builder.AddDbContext()); + + // add JsonApi fitlers and serializer + mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, options)); + + // register services + AddJsonApiInternals(services, options); + return services; } - public static IServiceCollection AddJsonApi( - this IServiceCollection services, - Action options, - IMvcCoreBuilder mvcBuilder) where TContext : DbContext + + /// + /// Enabling JsonApiDotNetCore using manual declaration to build the ResourceGraph. + /// + /// + /// + /// + public static IServiceCollection AddJsonApi(this IServiceCollection services, + Action configureOptions, + IMvcCoreBuilder mvcBuilder = null) { - var config = new JsonApiOptions(); - options(config); - config.BuildResourceGraph(builder => builder.AddDbContext()); + var options = _options; + mvcBuilder = mvcBuilder ?? services.AddMvcCore(); + configureOptions(options); - mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, config)); + // add JsonApi fitlers and serializer + mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, options)); - AddJsonApiInternals(services, config); + // register services + AddJsonApiInternals(services, options); return services; } - public static IServiceCollection AddJsonApi( - this IServiceCollection services, - Action configureOptions, - IMvcCoreBuilder mvcBuilder, - Action autoDiscover = null) + /// + /// Enabling JsonApiDotNetCore using the EF Core DbContext to build the ResourceGraph. + /// + /// + /// + /// + /// + public static IServiceCollection AddJsonApi(this IServiceCollection services, + Action configureOptions, + Action autoDiscover, + IMvcCoreBuilder mvcBuilder = null) { - var config = new JsonApiOptions(); - configureOptions(config); + var options = _options; + mvcBuilder = mvcBuilder ?? services.AddMvcCore(); + configureOptions(options); - if (autoDiscover != null) - { - var facade = new ServiceDiscoveryFacade(services, config.ResourceGraphBuilder); - autoDiscover(facade); - } + // build the resource graph using auto discovery. + var facade = new ServiceDiscoveryFacade(services, options.ResourceGraphBuilder); + autoDiscover(facade); - mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, config)); + // add JsonApi fitlers and serializer + mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, options)); - AddJsonApiInternals(services, config); + // register services + AddJsonApiInternals(services, options); return services; } + private static void AddMvcOptions(MvcOptions options, JsonApiOptions config) { options.Filters.Add(typeof(JsonApiExceptionFilter)); options.Filters.Add(typeof(TypeMatchFilter)); + options.Filters.Add(typeof(JsonApiActionFilter)); options.SerializeAsJsonApi(config); + } public static void AddJsonApiInternals( @@ -99,8 +151,8 @@ public static void AddJsonApiInternals( this IServiceCollection services, JsonApiOptions jsonApiOptions) { - if (jsonApiOptions.ResourceGraph == null) - jsonApiOptions.ResourceGraph = jsonApiOptions.ResourceGraphBuilder.Build(); + + var graph = jsonApiOptions.ResourceGraph ?? jsonApiOptions.ResourceGraphBuilder.Build(); if (jsonApiOptions.ResourceGraph.UsesDbContext == false) { @@ -109,7 +161,9 @@ public static void AddJsonApiInternals( } if (jsonApiOptions.EnableOperations) + { AddOperationServices(services); + } services.AddScoped(typeof(IEntityRepository<>), typeof(DefaultEntityRepository<>)); services.AddScoped(typeof(IEntityRepository<,>), typeof(DefaultEntityRepository<,>)); @@ -117,8 +171,6 @@ public static void AddJsonApiInternals( services.AddScoped(typeof(IEntityReadRepository<,>), typeof(DefaultEntityRepository<,>)); services.AddScoped(typeof(IEntityWriteRepository<,>), typeof(DefaultEntityRepository<,>)); - - services.AddScoped(typeof(ICreateService<>), typeof(EntityResourceService<>)); services.AddScoped(typeof(ICreateService<,>), typeof(EntityResourceService<,>)); @@ -140,25 +192,41 @@ public static void AddJsonApiInternals( services.AddScoped(typeof(IResourceService<>), typeof(EntityResourceService<>)); services.AddScoped(typeof(IResourceService<,>), typeof(EntityResourceService<,>)); - services.AddSingleton(jsonApiOptions); - services.AddSingleton(jsonApiOptions.ResourceGraph); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(typeof(IMetaBuilder<>), typeof(MetaBuilder<>)); + + services.AddSingleton(jsonApiOptions); + services.AddSingleton(jsonApiOptions); + services.AddSingleton(graph); services.AddSingleton(); + services.AddSingleton(graph); + + services.AddScoped(typeof(ServerSerializer<>)); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); 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(); - + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); if (jsonApiOptions.EnableResourceHooks) { @@ -169,7 +237,6 @@ public static void AddJsonApiInternals( } services.AddScoped(); - } private static void AddOperationServices(IServiceCollection services) @@ -194,9 +261,7 @@ private static void AddOperationServices(IServiceCollection services) 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)); } @@ -256,7 +321,6 @@ private static HashSet GetResourceTypesFromServiceImplementa } } } - return resourceDecriptors; } } diff --git a/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs b/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs index 023ef09ae2..587f3749f3 100644 --- a/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs @@ -1,5 +1,6 @@ using System; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.EntityFrameworkCore.Internal; @@ -29,10 +30,9 @@ public static ErrorCollection ConvertToErrorCollection(this ModelStateDiction meta: modelError.Exception != null ? ErrorMeta.FromException(modelError.Exception) : null, 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..2f8839cc74 100644 --- a/src/JsonApiDotNetCore/Extensions/StringExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/StringExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Text; namespace JsonApiDotNetCore.Extensions @@ -15,7 +16,7 @@ public static string ToProperCase(this string str) { if ((chars[i]) == '-') { - i = i + 1; + i++; builder.Append(char.ToUpper(chars[i])); } else @@ -50,5 +51,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/Formatters/JsonApiReader.cs b/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs index b305bf9722..94343094c4 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs @@ -3,26 +3,31 @@ using System.IO; using System.Threading.Tasks; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Services; +using JsonApiDotNetCore.Serialization.Contracts; 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 IOperationsDeserializer _operationsDeserializer; + private readonly IJsonApiDeserializer _deserializer; + private readonly IRequestContext _requestManager; private readonly ILogger _logger; - public JsonApiReader(IJsonApiDeSerializer deSerializer, IJsonApiContext jsonApiContext, ILoggerFactory loggerFactory) + public JsonApiReader(IJsonApiDeserializer deserializer, + IOperationsDeserializer operationsDeserializer, + IRequestContext requestManager, + ILoggerFactory loggerFactory) { - _deSerializer = deSerializer; - _jsonApiContext = jsonApiContext; + _deserializer = deserializer; + _operationsDeserializer = operationsDeserializer; + _requestManager = requestManager; _logger = loggerFactory.CreateLogger(); } @@ -39,17 +44,13 @@ public Task ReadAsync(InputFormatterContext context) { var body = GetRequestBody(context.HttpContext.Request.Body); - object model = null; - - if (_jsonApiContext.IsRelationshipPath) - { - model = _deSerializer.DeserializeRelationship(body); - } - else + if (_requestManager.IsBulkRequest) { - model = _deSerializer.Deserialize(body); + var operations = _operationsDeserializer.Deserialize(body); + return InputFormatterResult.SuccessAsync(operations); } + object model = _deserializer.Deserialize(body); if (model == null) { diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs b/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs index fcf0ac7850..fd55a2f0a1 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs @@ -1,6 +1,7 @@ using System; using System.Text; using System.Threading.Tasks; +using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Serialization; using Microsoft.AspNetCore.Mvc.Formatters; @@ -14,10 +15,10 @@ public class JsonApiWriter : IJsonApiWriter private readonly IJsonApiSerializer _serializer; public JsonApiWriter( - IJsonApiSerializer serializer, + IJsonApiSerializerFactory factory, ILoggerFactory loggerFactory) { - _serializer = serializer; + _serializer = factory.GetSerializer(); _logger = loggerFactory.CreateLogger(); } diff --git a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs index 99c3845108..d75cf9818e 100644 --- a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs @@ -1,9 +1,12 @@ using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Data; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using System; @@ -17,7 +20,7 @@ public class ServiceDiscoveryFacade { internal static HashSet ServiceInterfaces = new HashSet { typeof(IResourceService<>), - typeof(IResourceService<,>), + typeof(IResourceService<,>), typeof(IResourceCmdService<>), typeof(IResourceCmdService<,>), typeof(IResourceQueryService<>), @@ -46,13 +49,12 @@ public class ServiceDiscoveryFacade typeof(IEntityReadRepository<>), typeof(IEntityReadRepository<,>) }; - private readonly IServiceCollection _services; private readonly IResourceGraphBuilder _graphBuilder; private readonly List _identifiables = new List(); public ServiceDiscoveryFacade( - IServiceCollection services, + IServiceCollection services, IResourceGraphBuilder graphBuilder) { _services = services; @@ -80,13 +82,56 @@ public ServiceDiscoveryFacade AddAssembly(Assembly assembly) AddRepositories(assembly, resourceDescriptor); } + ScanControllers(assembly); + return this; } + private void ScanControllers(Assembly assembly) + { + var baseTypes = new List() { typeof(ControllerBase), typeof(JsonApiControllerMixin), typeof(JsonApiController<>), typeof(BaseJsonApiController<>) }; + List baseTypesSeen = new List() { }; + var types = assembly.GetTypes().ToList(); + //types.ForEach(t => baseTypesSeen.Add(t.BaseType.Name)); + + var controllerMapper = new Dictionary>() { }; + var undefinedMapper = new List() { }; + var sdf = assembly.GetTypes() + .Where(type => typeof(ControllerBase).IsAssignableFrom(type) & !type.IsGenericType).ToList(); + foreach (var controllerType in sdf) + { + // get generic parameter + var genericParameters = controllerType.BaseType.GetGenericArguments(); + if (genericParameters.Count() > 0) + { + + _graphBuilder.AddControllerPairing(controllerType, genericParameters[0]); + } + else + { + _graphBuilder.AddControllerPairing(controllerType); + } + } + } + + 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); @@ -125,7 +170,7 @@ 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) @@ -134,7 +179,7 @@ private void AddResourceToGraph(ResourceDescriptor identifiable) _graphBuilder.AddResource(identifiable.ResourceType, identifiable.IdType, resourceName); } - private string FormatResourceName(Type resourceType) + private string FormatResourceName(Type resourceType) => JsonApiOptions.ResourceNameFormatter.FormatResourceName(resourceType); /// @@ -145,45 +190,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. /// /// 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..519276dad9 100644 --- a/src/JsonApiDotNetCore/Hooks/Discovery/HooksDiscovery.cs +++ b/src/JsonApiDotNetCore/Hooks/Discovery/HooksDiscovery.cs @@ -66,7 +66,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/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..dd6602c3f9 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/DiffableEntityHashSet.cs +++ b/src/JsonApiDotNetCore/Hooks/Execution/DiffableEntityHashSet.cs @@ -6,7 +6,9 @@ using System.Reflection; using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Services; namespace JsonApiDotNetCore.Hooks @@ -52,9 +54,9 @@ public DiffableEntityHashSet(HashSet requestEntities, internal DiffableEntityHashSet(IEnumerable requestEntities, IEnumerable databaseEntities, Dictionary relationships, - IJsonApiContext jsonApiContext) + IUpdatedFields updatedFields) : this((HashSet)requestEntities, (HashSet)databaseEntities, TypeHelper.ConvertRelationshipDictionary(relationships), - TypeHelper.ConvertAttributeDictionary(jsonApiContext.AttributesToUpdate, (HashSet)requestEntities)) + TypeHelper.ConvertAttributeDictionary(updatedFields.Attributes, (HashSet)requestEntities)) { } @@ -90,7 +92,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..0dcf21f58d 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/EntityHashSet.cs +++ b/src/JsonApiDotNetCore/Hooks/Execution/EntityHashSet.cs @@ -27,8 +27,6 @@ public interface IEntityHashSet : IByAffectedRelationships /// public class EntityHashSet : HashSet, IEntityHashSet where TResource : class, IIdentifiable { - - /// public Dictionary> AffectedRelationships { get => _relationships; } private readonly RelationshipsDictionary _relationships; diff --git a/src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs b/src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs index df7f4daa0c..0867c62a19 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; @@ -10,30 +10,32 @@ using PrincipalType = System.Type; using DependentType = System.Type; using Microsoft.EntityFrameworkCore; -using JsonApiDotNetCore.Services; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Configuration; namespace JsonApiDotNetCore.Hooks { /// internal class HookExecutorHelper : IHookExecutorHelper { + private readonly IdentifiableComparer _comparer = new IdentifiableComparer(); + private readonly IJsonApiOptions _options; protected readonly IGenericProcessorFactory _genericProcessorFactory; protected readonly IResourceGraph _graph; protected readonly Dictionary _hookContainers; protected readonly Dictionary _hookDiscoveries; protected readonly List _targetedHooksForRelatedEntities; - protected readonly IJsonApiContext _context; public HookExecutorHelper( IGenericProcessorFactory genericProcessorFactory, IResourceGraph graph, - IJsonApiContext context + IJsonApiOptions options ) { + _options = options; _genericProcessorFactory = genericProcessorFactory; _graph = graph; - _context = context; _hookContainers = new Dictionary(); _hookDiscoveries = new Dictionary(); _targetedHooksForRelatedEntities = new List(); @@ -110,13 +112,13 @@ public bool ShouldLoadDbValues(Type entityType, ResourceHook hook) { return false; } - else if (discovery.DatabaseValuesEnabledHooks.Contains(hook)) + if (discovery.DatabaseValuesEnabledHooks.Contains(hook)) { return true; } else { - return _context.Options.LoadDatabaseValues; + return _options.LoaDatabaseValues; } } @@ -187,7 +189,7 @@ public Dictionary LoadImplicitlyAffected( dbDependentEntityList = (IList)relationshipValue; } var dbDependentEntityListCasted = dbDependentEntityList.Cast().ToList(); - if (existingDependentEntities != null) dbDependentEntityListCasted = dbDependentEntityListCasted.Except(existingDependentEntities.Cast(), ResourceHookExecutor.Comparer).ToList(); + if (existingDependentEntities != null) dbDependentEntityListCasted = dbDependentEntityListCasted.Except(existingDependentEntities.Cast(), _comparer).ToList(); if (dbDependentEntityListCasted.Any()) { @@ -219,4 +221,4 @@ bool IsHasManyThrough(KeyValuePair kvp, return (kvp.Key is HasManyThroughAttribute); } } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Hooks/Execution/RelationshipsDictionary.cs b/src/JsonApiDotNetCore/Hooks/Execution/RelationshipsDictionary.cs index b967e31464..33b9f0d163 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/RelationshipsDictionary.cs +++ b/src/JsonApiDotNetCore/Hooks/Execution/RelationshipsDictionary.cs @@ -48,7 +48,6 @@ public interface IRelationshipGetters where TResource : class, IIdent /// Gets a dictionary of all entities that have an affected relationship to type /// Dictionary> GetByRelationship(Type relatedResourceType); - /// /// Gets a collection of all the entities for the property within /// has been affected by the request diff --git a/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs index 338fa6ee6b..1ab795b415 100644 --- a/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs @@ -9,24 +9,31 @@ using DependentType = System.Type; using JsonApiDotNetCore.Services; using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Serialization; 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 ITraversalHelper _traversalHelper; + private readonly IIncludedQueryService _includedQuery; + private readonly IUpdatedFields _updatedFields; private readonly IResourceGraph _graph; - - public ResourceHookExecutor(IHookExecutorHelper helper, IJsonApiContext context, IResourceGraph graph) + public ResourceHookExecutor( + IHookExecutorHelper executorHelper, + ITraversalHelper traversalHelper, + IUpdatedFields updatedFields, + IIncludedQueryService includedRelationships, + IResourceGraph resourceGraph) { - _executorHelper = helper; - _context = context; - _graph = graph; - _traversalHelper = new TraversalHelper(graph, _context); + _executorHelper = executorHelper; + _traversalHelper = traversalHelper; + _updatedFields = updatedFields; + _includedQuery = includedRelationships; + _graph = resourceGraph; } /// @@ -34,12 +41,9 @@ public virtual void BeforeRead(ResourcePipeline pipeline, string string { 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); - } + foreach (var chain in _includedQuery.Get()) + RecursiveBeforeRead(chain, pipeline, calledContainers); } /// @@ -49,7 +53,7 @@ public virtual IEnumerable BeforeUpdate(IEnumerable e { 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); + var diff = new DiffableEntityHashSet(node.UniqueEntities, dbValues, node.PrincipalsToNextLayer(), _updatedFields); IEnumerable updated = container.BeforeUpdate(diff, pipeline); node.UpdateUnique(updated); node.Reassign(entities); @@ -187,12 +191,12 @@ bool GetHook(ResourceHook target, IEnumerable entities, } /// - /// 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 hookContainer = _executorHelper.GetResourceHookContainer(entityType, target); @@ -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}"); - } - + var relationship = relationshipChain.First(); if (!calledContainers.Contains(relationship.DependentType)) { calledContainers.Add(relationship.DependentType); var container = _executorHelper.GetResourceHookContainer(relationship.DependentType, 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,9 +237,9 @@ 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); IEnumerable uniqueEntities = node.UniqueEntities; @@ -447,7 +439,7 @@ 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(); diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/ChildNode.cs b/src/JsonApiDotNetCore/Hooks/Traversal/ChildNode.cs index 443ee7daf1..8a29d6c539 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/ChildNode.cs +++ b/src/JsonApiDotNetCore/Hooks/Traversal/ChildNode.cs @@ -1,15 +1,20 @@ -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; namespace JsonApiDotNetCore.Hooks { - /// - internal class ChildNode : IEntityNode where TEntity : class, IIdentifiable + /// + /// Child node in the tree + /// + /// + internal class ChildNode : INode where TEntity : class, IIdentifiable { + private readonly IdentifiableComparer _comparer = new IdentifiableComparer(); /// public DependentType EntityType { get; private set; } /// @@ -47,11 +52,14 @@ public void UpdateUnique(IEnumerable updated) List casted = updated.Cast().ToList(); foreach (var rpfl in _relationshipsFromPreviousLayer) { - rpfl.DependentEntities = new HashSet(rpfl.DependentEntities.Intersect(casted, ResourceHookExecutor.Comparer).Cast()); + rpfl.DependentEntities = new HashSet(rpfl.DependentEntities.Intersect(casted, _comparer).Cast()); } } - /// + /// + /// Reassignment is done according to provided relationships + /// + /// public void Reassign(IEnumerable updated = null) { var unique = (HashSet)UniqueEntities; @@ -66,12 +74,12 @@ public void Reassign(IEnumerable updated = null) if (currentValue is IEnumerable relationshipCollection) { - var newValue = relationshipCollection.Intersect(unique, ResourceHookExecutor.Comparer).Cast(proxy.DependentType); + var newValue = relationshipCollection.Intersect(unique, _comparer).Cast(proxy.DependentType); proxy.SetValue(principal, 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); } @@ -80,6 +88,4 @@ public void Reassign(IEnumerable updated = null) } } } - - } diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/IEntityNode.cs b/src/JsonApiDotNetCore/Hooks/Traversal/IEntityNode.cs index 159b373ef5..88c664a744 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/IEntityNode.cs +++ b/src/JsonApiDotNetCore/Hooks/Traversal/IEntityNode.cs @@ -1,9 +1,12 @@ -using System.Collections; +using System.Collections; using DependentType = 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. @@ -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..f0449b9756 --- /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 TEntity : class, IIdentifiable; + } +} diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/RootNode.cs b/src/JsonApiDotNetCore/Hooks/Traversal/RootNode.cs index 7783d041e1..1c4d4d6c1a 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/RootNode.cs +++ b/src/JsonApiDotNetCore/Hooks/Traversal/RootNode.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; +using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Hooks @@ -11,8 +12,9 @@ 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 TEntity : class, IIdentifiable { + private readonly IdentifiableComparer _comparer = new IdentifiableComparer(); private readonly RelationshipProxy[] _allRelationshipsToNextLayer; private HashSet _uniqueEntities; public Type EntityType { get; internal set; } @@ -54,7 +56,7 @@ public RootNode(IEnumerable uniqueEntities, RelationshipProxy[] poplate public void UpdateUnique(IEnumerable updated) { var casted = updated.Cast().ToList(); - var intersected = _uniqueEntities.Intersect(casted, ResourceHookExecutor.Comparer).Cast(); + var intersected = _uniqueEntities.Intersect(casted, _comparer).Cast(); _uniqueEntities = new HashSet(intersected); } diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs b/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs index 5061cce9bf..66d7973e3e 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs @@ -1,12 +1,14 @@ -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.Managers.Contracts; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; +using JsonApiDotNetCore.Serialization; using DependentType = System.Type; using PrincipalType = System.Type; @@ -19,12 +21,12 @@ 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 IContextEntityProvider _provider; + private readonly IUpdatedFields _updatedFields; /// /// Keeps track of which entities has already been traversed through, to prevent /// infinite loops in eg cyclic data structures. @@ -35,13 +37,12 @@ internal class TraversalHelper /// See the latter for more details. /// private readonly Dictionary RelationshipProxies = new Dictionary(); - public TraversalHelper( - IResourceGraph graph, - IJsonApiContext context) + IContextEntityProvider provider, + IUpdatedFields updatedFields) { - _context = context; - _graph = graph; + _updatedFields = updatedFields; + _provider = provider; } /// @@ -67,9 +68,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,7 +78,7 @@ 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 @@ -106,7 +107,7 @@ public EntityChildLayer CreateNextLayer(IEnumerable nodes) }).ToList(); /// wrap the child nodes in a EntityChildLayer - return new EntityChildLayer(nextNodes); + return new NodeLayer(nextNodes); } /// @@ -122,7 +123,7 @@ Dictionary - (Dictionary>, Dictionary>) ExtractEntities(IEnumerable principalNodes) + (Dictionary>, Dictionary>) ExtractEntities(IEnumerable principalNodes) { var principalsEntitiesGrouped = new Dictionary>(); // RelationshipAttr_prevlayer->currentlayer => prevLayerEntities var dependentsEntitiesGrouped = new Dictionary>(); // RelationshipAttr_prevlayer->currentlayer => currentLayerEntities @@ -200,7 +201,7 @@ HashSet ProcessEntities(IEnumerable incomingEntities) /// The type to parse void RegisterRelationshipProxies(DependentType type) { - var contextEntity = _graph.GetContextEntity(type); + var contextEntity = _provider.GetContextEntity(type); foreach (RelationshipAttribute attr in contextEntity.Relationships) { if (!attr.CanInclude) continue; @@ -208,7 +209,8 @@ void RegisterRelationshipProxies(DependentType type) { DependentType dependentType = GetDependentTypeFromRelationship(attr); bool isContextRelation = false; - if (_context.RelationshipsToUpdate != null) isContextRelation = _context.RelationshipsToUpdate.ContainsKey(attr); + var relationshipsToUpdate = _updatedFields.Relationships; + if (relationshipsToUpdate != null) isContextRelation = relationshipsToUpdate.Contains(attr); var proxy = new RelationshipProxy(attr, dependentType, isContextRelation); RelationshipProxies[attr] = proxy; } @@ -251,7 +253,7 @@ HashSet GetProcessedEntities(Type entityType) /// Entity type. HashSet UniqueInTree(IEnumerable entities, Type entityType) where TEntity : class, IIdentifiable { - var newEntities = entities.Except(GetProcessedEntities(entityType), ResourceHookExecutor.Comparer).Cast(); + var newEntities = entities.Except(GetProcessedEntities(entityType), _comparer).Cast(); return new HashSet(newEntities); } @@ -285,10 +287,10 @@ void AddToRelationshipGroup(Dictionary> t /// /// Reflective helper method to create an instance of ; /// - IEntityNode CreateNodeInstance(DependentType nodeType, RelationshipProxy[] relationshipsToNext, IEnumerable relationshipsFromPrev) + INode CreateNodeInstance(DependentType 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 }); } /// @@ -316,21 +318,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/CamelizedRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/CamelizedRoutingConvention.cs new file mode 100644 index 0000000000..edb7e2444a --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/CamelizedRoutingConvention.cs @@ -0,0 +1,40 @@ +// 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/ContextEntity.cs b/src/JsonApiDotNetCore/Internal/ContextEntity.cs index 867a04350c..d873da893a 100644 --- a/src/JsonApiDotNetCore/Internal/ContextEntity.cs +++ b/src/JsonApiDotNetCore/Internal/ContextEntity.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.Links; namespace JsonApiDotNetCore.Internal { @@ -30,18 +32,44 @@ public string EntityName { public Type ResourceType { get; set; } /// - /// Exposed resource attributes + /// Exposed resource attributes. + /// See https://jsonapi.org/format/#document-resource-object-attributes. /// public List Attributes { get; set; } /// - /// Exposed resource relationships + /// Exposed resource relationships. + /// See https://jsonapi.org/format/#document-resource-object-relationships /// public List Relationships { get; set; } + private List _fields; + public List Fields { get { _fields = _fields ?? Attributes.Cast().Concat(Relationships).ToList(); return _fields; } } + + /// + /// 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; + /// - /// Links to include in resource responses + /// Configures which links to show in the + /// object for this resource. If set to , + /// the configuration will be read from . + /// Defaults to . /// - public Link Links { get; set; } = Link.All; + 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/Contracts/IContextEntityProvider.cs b/src/JsonApiDotNetCore/Internal/Contracts/IContextEntityProvider.cs new file mode 100644 index 0000000000..cf438f9667 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Contracts/IContextEntityProvider.cs @@ -0,0 +1,23 @@ +using System; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Internal.Contracts +{ + public interface IContextEntityProvider + { + /// + /// Get the resource metadata by the DbSet property name + /// + ContextEntity GetContextEntity(string exposedResourceName); + + /// + /// Get the resource metadata by the resource type + /// + ContextEntity GetContextEntity(Type resourceType); + + /// + /// Get the resource metadata by the resource type + /// + ContextEntity GetContextEntity() where TResource : class, IIdentifiable; + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Internal/Contracts/IResourceGraph.cs b/src/JsonApiDotNetCore/Internal/Contracts/IResourceGraph.cs new file mode 100644 index 0000000000..ded97b615a --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Contracts/IResourceGraph.cs @@ -0,0 +1,73 @@ +using JsonApiDotNetCore.Models; +using System; +using System.Collections.Generic; +using System.Text; + +namespace JsonApiDotNetCore.Internal.Contracts +{ + /// + /// A cache for the models in entity core + /// + public interface IResourceGraph : IContextEntityProvider + { + RelationshipAttribute GetInverseRelationship(RelationshipAttribute relationship); + /// + /// 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 public attribute name for a type based on the internal attribute name. + /// + /// The internal attribute name for a . + string GetPublicAttributeName(string internalAttributeName); + + /// + /// Was built against an EntityFrameworkCore DbContext ? + /// + bool UsesDbContext { get; } + + ContextEntity GetEntityFromControllerName(string pathParsed); + } +} diff --git a/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs index edb7e2444a..21033838d4 100644 --- a/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs @@ -7,10 +7,10 @@ namespace JsonApiDotNetCore.Internal { - public class DasherizedRoutingConvention : IApplicationModelConvention + public class CamelizedRoutingConvention : IApplicationModelConvention { private readonly string _namespace; - public DasherizedRoutingConvention(string nspace) + public CamelizedRoutingConvention(string nspace) { _namespace = nspace; } @@ -19,10 +19,10 @@ public void Apply(ApplicationModel application) { foreach (var controller in application.Controllers) { - if (IsDasherizedJsonApiController(controller) == false) + if (IsCamelizedJsonApiController(controller) == false) continue; - var template = $"{_namespace}/{controller.ControllerName.Dasherize()}"; + var template = $"{_namespace}/{controller.ControllerName.Camelize()}"; controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel { Template = template @@ -30,7 +30,7 @@ public void Apply(ApplicationModel application) } } - private bool IsDasherizedJsonApiController(ControllerModel controller) + private bool IsCamelizedJsonApiController(ControllerModel controller) { var type = controller.ControllerType; var notDisabled = type.GetCustomAttribute() == null; 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 100% rename from src/JsonApiDotNetCore/Internal/JsonApiException.cs rename to src/JsonApiDotNetCore/Internal/Exceptions/JsonApiException.cs 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/JsonApiRouteHandler.cs b/src/JsonApiDotNetCore/Internal/Exceptions/JsonApiRouteHandler.cs similarity index 100% rename from src/JsonApiDotNetCore/Internal/JsonApiRouteHandler.cs rename to src/JsonApiDotNetCore/Internal/Exceptions/JsonApiRouteHandler.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/IdentifiableComparer.cs b/src/JsonApiDotNetCore/Internal/IdentifiableComparer.cs index a830e2aec5..273f5f5d51 100644 --- a/src/JsonApiDotNetCore/Internal/IdentifiableComparer.cs +++ b/src/JsonApiDotNetCore/Internal/IdentifiableComparer.cs @@ -8,9 +8,9 @@ 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..fbfd4b9da7 100644 --- a/src/JsonApiDotNetCore/Internal/InverseRelationships.cs +++ b/src/JsonApiDotNetCore/Internal/InverseRelationships.cs @@ -1,5 +1,6 @@ using System; using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; 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 index 3205e1e01a..ee62dcbfb0 100644 --- a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs @@ -1,4 +1,6 @@ using System; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; @@ -7,9 +9,10 @@ namespace JsonApiDotNetCore.Internal.Query public class AttrFilterQuery : BaseFilterQuery { public AttrFilterQuery( - IJsonApiContext jsonApiContext, + IRequestContext requestManager, + IResourceGraph resourceGraph, FilterQuery filterQuery) - : base(jsonApiContext, filterQuery) + : base(requestManager, resourceGraph, filterQuery) { if (Attribute == null) throw new JsonApiException(400, $"'{filterQuery.Attribute}' is not a valid attribute."); diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs index 341b7e15c0..d60825b503 100644 --- a/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs @@ -4,10 +4,8 @@ namespace JsonApiDotNetCore.Internal.Query { public class AttrSortQuery : BaseAttrQuery { - public AttrSortQuery( - IJsonApiContext jsonApiContext, - SortQuery sortQuery) - :base(jsonApiContext, sortQuery) + public AttrSortQuery(IJsonApiContext jsonApiContext ,SortQuery sortQuery) + :base(jsonApiContext.RequestManager,jsonApiContext.ResourceGraph, sortQuery) { if (Attribute == null) throw new JsonApiException(400, $"'{sortQuery.Attribute}' is not a valid attribute."); diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs index e7eeb83e68..5e5bdb1b43 100644 --- a/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs @@ -1,3 +1,5 @@ +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using System; @@ -12,24 +14,19 @@ namespace JsonApiDotNetCore.Internal.Query /// public abstract class BaseAttrQuery { - private readonly IJsonApiContext _jsonApiContext; + private readonly IRequestContext _requestManager; + private readonly IResourceGraph _resourceGraph; - public BaseAttrQuery(IJsonApiContext jsonApiContext, BaseQuery baseQuery) + public BaseAttrQuery(IRequestContext requestManager, IResourceGraph resourceGraph, 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)); + _requestManager = requestManager ?? throw new ArgumentNullException(nameof(requestManager)); + _resourceGraph = resourceGraph ?? throw new ArgumentNullException(nameof(resourceGraph)); - if(_jsonApiContext.ResourceGraph == null) - throw new ArgumentException($"{nameof(IJsonApiContext)}.{nameof(_jsonApiContext.ResourceGraph)} cannot be null. " + if(_resourceGraph == null) + throw new ArgumentException($"{nameof(IJsonApiContext)}.{nameof(_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)); + + "https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/251", nameof(requestManager)); if (baseQuery.IsAttributeOfRelationship) { @@ -56,14 +53,18 @@ public string GetPropertyPath() } private AttrAttribute GetAttribute(string attribute) - => _jsonApiContext.RequestEntity.Attributes.FirstOrDefault(attr => attr.Is(attribute)); + { + return _requestManager.GetRequestResource().Attributes.FirstOrDefault(attr => attr.Is(attribute)); + } private RelationshipAttribute GetRelationship(string propertyName) - => _jsonApiContext.RequestEntity.Relationships.FirstOrDefault(r => r.Is(propertyName)); + { + return _requestManager.GetRequestResource().Relationships.FirstOrDefault(r => r.Is(propertyName)); + } private AttrAttribute GetAttribute(RelationshipAttribute relationship, string attribute) { - var relatedContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.DependentType); + var relatedContextEntity = _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 index f7e308369e..1347823158 100644 --- a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs @@ -1,14 +1,20 @@ +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Services; using System; namespace JsonApiDotNetCore.Internal.Query { + /// + /// Is the base for all filter queries + /// public class BaseFilterQuery : BaseAttrQuery { public BaseFilterQuery( - IJsonApiContext jsonApiContext, + IRequestContext requestManager, + IResourceGraph resourceGraph, FilterQuery filterQuery) - : base(jsonApiContext, filterQuery) + : base(requestManager, resourceGraph, filterQuery) { PropertyValue = filterQuery.Value; FilterOperation = GetFilterOperation(filterQuery.Operation); diff --git a/src/JsonApiDotNetCore/Internal/Query/PageQuery.cs b/src/JsonApiDotNetCore/Internal/Query/PageQuery.cs index 7c09d4c386..eb44cae170 100644 --- a/src/JsonApiDotNetCore/Internal/Query/PageQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/PageQuery.cs @@ -2,7 +2,7 @@ namespace JsonApiDotNetCore.Internal.Query { public class PageQuery { - public int PageSize { get; set; } - public int PageOffset { get; set; } = 1; + public int? PageSize { get; set; } + public int? PageOffset { get; set; } = 1; } } \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs index 8fef8de693..1d0073265a 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs @@ -1,4 +1,6 @@ using System; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; @@ -7,19 +9,21 @@ namespace JsonApiDotNetCore.Internal.Query public class RelatedAttrFilterQuery : BaseFilterQuery { public RelatedAttrFilterQuery( - IJsonApiContext jsonApiContext, + IRequestContext requestManager, + IResourceGraph resourceGraph, FilterQuery filterQuery) - :base(jsonApiContext, filterQuery) + : base(requestManager: requestManager, + resourceGraph: resourceGraph, + filterQuery: filterQuery) { if (Relationship == null) - throw new JsonApiException(400, $"{filterQuery.Relationship} is not a valid relationship on {jsonApiContext.RequestEntity.EntityName}."); + throw new JsonApiException(400, $"{filterQuery.Relationship} is not a valid relationship on {requestManager.GetRequestResource().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 index 4215382c80..8c54581693 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs @@ -7,7 +7,7 @@ public class RelatedAttrSortQuery : BaseAttrQuery public RelatedAttrSortQuery( IJsonApiContext jsonApiContext, SortQuery sortQuery) - :base(jsonApiContext, sortQuery) + :base(jsonApiContext.RequestManager, jsonApiContext.ResourceGraph, sortQuery) { if (Relationship == null) throw new JsonApiException(400, $"{sortQuery.Relationship} is not a valid relationship on {jsonApiContext.RequestEntity.EntityName}."); diff --git a/src/JsonApiDotNetCore/Internal/ResourceGraph.cs b/src/JsonApiDotNetCore/Internal/ResourceGraph.cs index efec077536..b1b408b3bd 100644 --- a/src/JsonApiDotNetCore/Internal/ResourceGraph.cs +++ b/src/JsonApiDotNetCore/Internal/ResourceGraph.cs @@ -2,93 +2,32 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Internal { - public interface IResourceGraph + public class ControllerResourceMap { - /// - /// 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; } + public string ControllerName { get; set; } + public Type Resource { get; set; } } + /// + /// keeps track of all the models/resources defined in JADNC + /// public class ResourceGraph : IResourceGraph { internal List Entities { get; } internal List ValidationResults { get; } + + public List ControllerResourceMap { get; internal set; } + + [Obsolete("please instantiate properly, dont use the static constructor")] internal static IResourceGraph Instance { get; set; } public ResourceGraph() { } + [Obsolete("Use new one")] public ResourceGraph(List entities, bool usesDbContext) { Entities = entities; @@ -101,8 +40,9 @@ public ResourceGraph(List entities, bool usesDbContext) // 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) + internal ResourceGraph(List entities, bool usesDbContext, List validationResults, List controllerContexts) { + ControllerResourceMap = controllerContexts; Entities = entities; UsesDbContext = usesDbContext; ValidationResults = validationResults; @@ -112,14 +52,6 @@ internal ResourceGraph(List entities, bool usesDbContext, List public bool UsesDbContext { get; } - /// - public ContextEntity GetContextEntity(string entityName) - => Entities.SingleOrDefault(e => string.Equals(e.EntityName, entityName, StringComparison.OrdinalIgnoreCase)); - - /// - public ContextEntity GetContextEntity(Type entityType) - => Entities.SingleOrDefault(e => e.EntityType == entityType); - /// public object GetRelationship(TParent entity, string relationshipName) { @@ -137,7 +69,7 @@ public object GetRelationship(TParent entity, string relationshipName) public object GetRelationshipValue(TParent resource, RelationshipAttribute relationship) where TParent : IIdentifiable { - if(relationship is HasManyThroughAttribute hasManyThroughRelationship) + if (relationship is HasManyThroughAttribute hasManyThroughRelationship) { return GetHasManyThrough(resource, hasManyThroughRelationship); } @@ -189,5 +121,33 @@ public RelationshipAttribute GetInverseRelationship(RelationshipAttribute relati if (relationship.InverseNavigation == null) return null; return GetContextEntity(relationship.DependentType).Relationships.SingleOrDefault(r => r.InternalRelationshipName == relationship.InverseNavigation); } + + public ContextEntity GetEntityFromControllerName(string controllerName) + { + + if (ControllerResourceMap.Any()) + { + // Autodiscovery was used, so there is a well defined mapping between exposed resources and their associated controllers + var resourceType = ControllerResourceMap.FirstOrDefault(cm => cm.ControllerName == controllerName)?.Resource; + if (resourceType == null) return null; + return Entities.First(e => e.EntityType == resourceType); + + } else + { + // No autodiscovery: try to guess contextentity from controller name. + return Entities.FirstOrDefault(e => e.EntityName.ToLower().Replace("-", "") == controllerName.ToLower()); + } + } + + /// + public ContextEntity GetContextEntity(string entityName) + => Entities.SingleOrDefault(e => string.Equals(e.EntityName, entityName, StringComparison.OrdinalIgnoreCase)); + + /// + public ContextEntity GetContextEntity(Type entityType) + => Entities.SingleOrDefault(e => e.EntityType == entityType); + /// + public ContextEntity GetContextEntity() where TResource : class, IIdentifiable + => GetContextEntity(typeof(TResource)); } } diff --git a/src/JsonApiDotNetCore/Internal/RouteMatcher.cs b/src/JsonApiDotNetCore/Internal/RouteMatcher.cs new file mode 100644 index 0000000000..4c5771ade1 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/RouteMatcher.cs @@ -0,0 +1,10 @@ +using System; +namespace JsonApiDotNetCore.Internal +{ + public class RouteMatcher + { + public RouteMatcher() + { + } + } +} diff --git a/src/JsonApiDotNetCore/Internal/TypeHelper.cs b/src/JsonApiDotNetCore/Internal/TypeHelper.cs index fb2c7df973..47024c63c0 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); @@ -53,7 +59,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); } } @@ -156,9 +162,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/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index dcfe030039..4b2f67bd28 100644 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -27,7 +27,7 @@ - + @@ -45,6 +45,12 @@ - + + + + + + + diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiActionFilter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiActionFilter.cs new file mode 100644 index 0000000000..aa8e00c8dd --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/JsonApiActionFilter.cs @@ -0,0 +1,129 @@ +using System; +using System.Linq; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; + +namespace JsonApiDotNetCore.Middleware +{ + public class JsonApiActionFilter : IActionFilter + { + private readonly IResourceGraph _resourceGraph; + private readonly IRequestContext _requestManager; + private readonly IPageQueryService _pageManager; + private readonly IQueryParser _queryParser; + private readonly IJsonApiOptions _options; + private HttpContext _httpContext; + public JsonApiActionFilter(IResourceGraph resourceGraph, + IRequestContext requestManager, + IPageQueryService pageManager, + IQueryParser queryParser, + IJsonApiOptions options) + { + _resourceGraph = resourceGraph; + _requestManager = requestManager; + _pageManager = pageManager; + _queryParser = queryParser; + _options = options; + } + + /// + /// + public void OnActionExecuting(ActionExecutingContext context) + { + _httpContext = context.HttpContext; + ContextEntity contextEntityCurrent = GetCurrentEntity(); + + // the contextEntity is null eg when we're using a non-JsonApiDotNetCore route. + if (contextEntityCurrent != null) + { + _requestManager.SetRequestResource(contextEntityCurrent); + _requestManager.BasePath = GetBasePath(contextEntityCurrent.EntityName); + HandleUriParameters(); + } + + } + + /// + /// Parses the uri + /// + protected void HandleUriParameters() + { + if (_httpContext.Request.Query.Count > 0) + { + var querySet = _queryParser.Parse(_httpContext.Request.Query); + _requestManager.QuerySet = querySet; //this shouldn't be exposed? + _pageManager.PageSize = querySet.PageQuery.PageSize ?? _pageManager.PageSize; + _pageManager.CurrentPage = querySet.PageQuery.PageOffset ?? _pageManager.CurrentPage; + + } + } + + private string GetBasePath(string entityName) + { + var r = _httpContext.Request; + if (_options.RelativeLinks) + { + return GetNamespaceFromPath(r.Path, entityName); + } + else + { + 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; + } + /// + /// Gets the current entity that we need for serialization and deserialization. + /// + /// + /// + /// + private ContextEntity GetCurrentEntity() + { + var controllerName = (string)_httpContext.GetRouteData().Values["controller"]; + return _resourceGraph.GetEntityFromControllerName(controllerName); + } + + + private bool IsJsonApiRequest(HttpRequest request) + { + return (request.ContentType?.Equals(Constants.ContentType, StringComparison.OrdinalIgnoreCase) == true); + } + + public void OnActionExecuted(ActionExecutedContext context) { /* noop */ } + } +} diff --git a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs index ab472a8dba..53555ccfb7 100644 --- a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs @@ -1,40 +1,62 @@ using System; +using System.Linq; using System.Threading.Tasks; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Primitives; namespace JsonApiDotNetCore.Middleware { + /// + /// Can be overwritten to help you out during testing + /// + /// This sets all necessary parameters relating to the HttpContext for JADNC + /// public class RequestMiddleware { private readonly RequestDelegate _next; + private HttpContext _httpContext; + private IRequestContext _requestManager; public RequestMiddleware(RequestDelegate next) { _next = next; } - public async Task Invoke(HttpContext context, IJsonApiContext jsonApiContext) + public async Task Invoke(HttpContext httpContext, + IRequestContext requestManager) { - if (IsValid(context)) + _httpContext = httpContext; + _requestManager = requestManager; + + if (IsValid()) { - // 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); + _requestManager.IsRelationshipPath = PathIsRelationship(); + _requestManager.IsBulkRequest = PathIsBulk(); + await _next(httpContext); } } - private static bool IsValid(HttpContext context) + private bool PathIsBulk() + { + var actionName = (string)_httpContext.GetRouteData().Values["action"]; + return actionName.ToLower().Contains("bulk"); + } + + protected bool PathIsRelationship() + { + var actionName = (string)_httpContext.GetRouteData().Values["action"]; + return actionName.ToLower().Contains("relationships"); + } + private bool IsValid() { - return IsValidContentTypeHeader(context) && IsValidAcceptHeader(context); + return IsValidContentTypeHeader(_httpContext) && IsValidAcceptHeader(_httpContext); } - private static bool IsValidContentTypeHeader(HttpContext context) + private bool IsValidContentTypeHeader(HttpContext context) { var contentType = context.Request.ContentType; if (contentType != null && ContainsMediaTypeParameters(contentType)) @@ -45,7 +67,7 @@ private static bool IsValidContentTypeHeader(HttpContext context) return true; } - private static bool IsValidAcceptHeader(HttpContext context) + private bool IsValidAcceptHeader(HttpContext context) { if (context.Request.Headers.TryGetValue(Constants.AcceptHeader, out StringValues acceptHeaders) == false) return true; @@ -80,7 +102,7 @@ internal static bool ContainsMediaTypeParameters(string mediaType) ); } - private static void FlushResponse(HttpContext context, int statusCode) + private void FlushResponse(HttpContext context, int statusCode) { context.Response.StatusCode = statusCode; context.Response.Body.Flush(); diff --git a/src/JsonApiDotNetCore/Middleware/TypeMatchFilter.cs b/src/JsonApiDotNetCore/Middleware/TypeMatchFilter.cs index 64624dfa70..e3e474740e 100644 --- a/src/JsonApiDotNetCore/Middleware/TypeMatchFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/TypeMatchFilter.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Filters; @@ -9,11 +10,11 @@ namespace JsonApiDotNetCore.Middleware { public class TypeMatchFilter : IActionFilter { - private readonly IJsonApiContext _jsonApiContext; + private readonly IResourceGraph _resourceGraph; - public TypeMatchFilter(IJsonApiContext jsonApiContext) + public TypeMatchFilter(IResourceGraph resourceGraph) { - _jsonApiContext = jsonApiContext; + _resourceGraph = resourceGraph; } /// @@ -29,10 +30,10 @@ public void OnActionExecuting(ActionExecutingContext context) if (deserializedType != null && targetType != null && deserializedType != targetType) { - var expectedJsonApiResource = _jsonApiContext.ResourceGraph.GetContextEntity(targetType); + var expectedJsonApiResource = _resourceGraph.GetContextEntity(targetType); throw new JsonApiException(409, - $"Cannot '{context.HttpContext.Request.Method}' type '{_jsonApiContext.RequestEntity.EntityName}' " + $"Cannot '{context.HttpContext.Request.Method}' type '{deserializedType.Name}' " + $"to '{expectedJsonApiResource?.EntityName}' endpoint.", detail: "Check that the request payload type matches the type expected by this endpoint."); } diff --git a/src/JsonApiDotNetCore/Models/AttrAttribute.cs b/src/JsonApiDotNetCore/Models/AttrAttribute.cs index 6d48098192..da3a9d4631 100644 --- a/src/JsonApiDotNetCore/Models/AttrAttribute.cs +++ b/src/JsonApiDotNetCore/Models/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/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/HasManyAttribute.cs b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs index 37fe42c9af..a69cb82c56 100644 --- a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs +++ b/src/JsonApiDotNetCore/Models/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,8 +25,8 @@ 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; } diff --git a/src/JsonApiDotNetCore/Models/HasManyThroughAttribute.cs b/src/JsonApiDotNetCore/Models/HasManyThroughAttribute.cs index c6f5bc47db..235607c6d3 100644 --- a/src/JsonApiDotNetCore/Models/HasManyThroughAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasManyThroughAttribute.cs @@ -1,6 +1,6 @@ using System; using System.Reflection; -using System.Security; +using JsonApiDotNetCore.Models.Links; namespace JsonApiDotNetCore.Models { @@ -29,7 +29,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 +38,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; } diff --git a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs index 54a024e703..e9d0883180 100644 --- a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs @@ -1,5 +1,6 @@ using System; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Models.Links; namespace JsonApiDotNetCore.Models { @@ -10,7 +11,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,11 +28,10 @@ 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; @@ -57,8 +58,13 @@ public override void SetValue(object resource, object newValue) // 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); + var resourceType = resource.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(resource, newValue); } diff --git a/src/JsonApiDotNetCore/Models/IHasMeta.cs b/src/JsonApiDotNetCore/Models/IHasMeta.cs index 50d86e6034..34efffdabd 100644 --- a/src/JsonApiDotNetCore/Models/IHasMeta.cs +++ b/src/JsonApiDotNetCore/Models/IHasMeta.cs @@ -5,6 +5,6 @@ namespace JsonApiDotNetCore.Models { public interface IHasMeta { - Dictionary GetMeta(IJsonApiContext context); + Dictionary GetMeta(); } } diff --git a/src/JsonApiDotNetCore/Models/IResourceField.cs b/src/JsonApiDotNetCore/Models/IResourceField.cs new file mode 100644 index 0000000000..90ec306c93 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/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/ISerializableFields.cs b/src/JsonApiDotNetCore/Models/ISerializableFields.cs new file mode 100644 index 0000000000..3a8a6a771f --- /dev/null +++ b/src/JsonApiDotNetCore/Models/ISerializableFields.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace JsonApiDotNetCore.Models +{ + /// TODO: GetOutputAttrs is used in SERIALIATION LAYER to remove fields from + /// list of attrs that will be displayed, (i.e. touches the DOCUMENT structure) + /// whereas hooks is stuff to do on the MODEL in the SERVICELAYER. + /// Consider (not sure yet) to move to different class because of this. + /// edit: using different interfaces for this is maybe good enough to separate + public interface ISerializableFields + { + List GetAllowedAttributes(Type type); + List GetAllowedRelationships(Type type); + } +} diff --git a/src/JsonApiDotNetCore/Models/JsonApi/Document.cs b/src/JsonApiDotNetCore/Models/JsonApi/Document.cs new file mode 100644 index 0000000000..0b74574ee9 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/JsonApi/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/JsonApi/ExposableData.cs b/src/JsonApiDotNetCore/Models/JsonApi/ExposableData.cs new file mode 100644 index 0000000000..7fc8d8cdae --- /dev/null +++ b/src/JsonApiDotNetCore/Models/JsonApi/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(RelationshipData)) + 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 HasData { 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/JsonApi/IIdentifiable.cs similarity index 100% rename from src/JsonApiDotNetCore/Models/IIdentifiable.cs rename to src/JsonApiDotNetCore/Models/JsonApi/IIdentifiable.cs diff --git a/src/JsonApiDotNetCore/Models/Identifiable.cs b/src/JsonApiDotNetCore/Models/JsonApi/Identifiable.cs similarity index 87% rename from src/JsonApiDotNetCore/Models/Identifiable.cs rename to src/JsonApiDotNetCore/Models/JsonApi/Identifiable.cs index b62f31fe89..b85128444e 100644 --- a/src/JsonApiDotNetCore/Models/Identifiable.cs +++ b/src/JsonApiDotNetCore/Models/JsonApi/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/JsonApi/Link.cs b/src/JsonApiDotNetCore/Models/JsonApi/Link.cs new file mode 100644 index 0000000000..5bba6273a0 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/JsonApi/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/JsonApi/RelationshipData.cs b/src/JsonApiDotNetCore/Models/JsonApi/RelationshipData.cs new file mode 100644 index 0000000000..65aa8d8f8c --- /dev/null +++ b/src/JsonApiDotNetCore/Models/JsonApi/RelationshipData.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Models.Links; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace JsonApiDotNetCore.Models +{ + public class RelationshipData : ExposableData + { + [JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)] + public RelationshipLinks Links { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Models/JsonApi/RelationshipLinks.cs b/src/JsonApiDotNetCore/Models/JsonApi/RelationshipLinks.cs new file mode 100644 index 0000000000..c728df6777 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/JsonApi/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/JsonApi/ResourceIdentifierObject.cs similarity index 58% rename from src/JsonApiDotNetCore/Models/ResourceIdentifierObject.cs rename to src/JsonApiDotNetCore/Models/JsonApi/ResourceIdentifierObject.cs index 2cf4ef401e..aac5af98be 100644 --- a/src/JsonApiDotNetCore/Models/ResourceIdentifierObject.cs +++ b/src/JsonApiDotNetCore/Models/JsonApi/ResourceIdentifierObject.cs @@ -11,13 +11,20 @@ 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")] + [JsonIgnore] + //[JsonProperty("lid")] public string LocalId { get; set; } + + + public override string ToString() + { + return $"(type: {Type}, id: {Id})"; + } } } diff --git a/src/JsonApiDotNetCore/Models/JsonApi/ResourceLinks.cs b/src/JsonApiDotNetCore/Models/JsonApi/ResourceLinks.cs new file mode 100644 index 0000000000..ea701f7681 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/JsonApi/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/ResourceObject.cs b/src/JsonApiDotNetCore/Models/JsonApi/ResourceObject.cs similarity index 61% rename from src/JsonApiDotNetCore/Models/ResourceObject.cs rename to src/JsonApiDotNetCore/Models/JsonApi/ResourceObject.cs index 1a28631407..89ff2e9e9c 100644 --- a/src/JsonApiDotNetCore/Models/ResourceObject.cs +++ b/src/JsonApiDotNetCore/Models/JsonApi/ResourceObject.cs @@ -1,14 +1,18 @@ using System.Collections.Generic; +using JsonApiDotNetCore.Models.Links; using Newtonsoft.Json; namespace JsonApiDotNetCore.Models { public class ResourceObject : ResourceIdentifierObject { - [JsonProperty("attributes")] + [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/JsonApi/ResourceObjectComparer.cs b/src/JsonApiDotNetCore/Models/JsonApi/ResourceObjectComparer.cs new file mode 100644 index 0000000000..e7da59bb98 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/JsonApi/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/JsonApi/TopLevelLinks.cs similarity index 85% rename from src/JsonApiDotNetCore/Models/RootLinks.cs rename to src/JsonApiDotNetCore/Models/JsonApi/TopLevelLinks.cs index 42b0a7863f..22c8d12f16 100644 --- a/src/JsonApiDotNetCore/Models/RootLinks.cs +++ b/src/JsonApiDotNetCore/Models/JsonApi/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 index 85e2693111..1c071723f1 100644 --- a/src/JsonApiDotNetCore/Models/LinksAttribute.cs +++ b/src/JsonApiDotNetCore/Models/LinksAttribute.cs @@ -1,14 +1,43 @@ using System; +using JsonApiDotNetCore.Internal; -namespace JsonApiDotNetCore.Models +namespace JsonApiDotNetCore.Models.Links { + [AttributeUsage(AttributeTargets.Class, Inherited = false)] public class LinksAttribute : Attribute { - public LinksAttribute(Link links) + public LinksAttribute(Link topLevelLinks = Link.NotConfigured, Link resourceLinks = Link.NotConfigured, Link relationshipLinks = Link.NotConfigured) { - Links = links; + 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; } - public Link Links { get; set; } + /// + /// 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/Operations/Operation.cs b/src/JsonApiDotNetCore/Models/Operations/Operation.cs index 604643d231..be6310c0da 100644 --- a/src/JsonApiDotNetCore/Models/Operations/Operation.cs +++ b/src/JsonApiDotNetCore/Models/Operations/Operation.cs @@ -1,12 +1,22 @@ using System.Collections.Generic; +using JsonApiDotNetCore.Models.Links; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; namespace JsonApiDotNetCore.Models.Operations { - public class Operation : DocumentBase + public class Operation { + [JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)] + public TopLevelLinks Links { get; set; } + + [JsonProperty("included", NullValueHandling = NullValueHandling.Ignore)] + public List Included { get; set; } + + [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] + public Dictionary Meta { get; set; } + [JsonProperty("op"), JsonConverter(typeof(StringEnumConverter))] public OperationCode Op { get; set; } diff --git a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs index 3bd44f5030..83d8b4043c 100644 --- a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Models/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 { - 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; } @@ -25,7 +30,7 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI /// /// /// - /// public List<Tag> Tags { get; set; } // Type => Tag + /// public List<Tag> Tags { get; sit; } // Type => Tag /// /// [Obsolete("Use property DependentType")] @@ -52,7 +57,12 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI 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; } @@ -119,5 +129,6 @@ public virtual bool Is(string publicRelationshipName) /// In all cases except the HasManyThrough relationships, this will just be the . /// public virtual string RelationshipPath => InternalRelationshipName; + } } 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..9961bb6d92 100644 --- a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs @@ -1,90 +1,51 @@ 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; +using JsonApiDotNetCore.Services; namespace JsonApiDotNetCore.Models { - public interface IResourceDefinition { - List GetOutputAttrs(object instance); + List GetAllowedAttributes(); + List GetAllowedRelationships(); } - /// /// 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 IExposedFieldExplorer _fieldExplorer; + private List _allowedAttributes; + private List _allowedRelationships; + public ResourceDefinition(IExposedFieldExplorer fieldExplorer, IResourceGraph graph) { - _contextEntity = graph.GetContextEntity(typeof(T)); - _instanceAttrsAreSpecified = InstanceOutputAttrsAreSpecified(); + _contextEntity = graph.GetContextEntity(typeof(TResource)); + _allowedAttributes = _contextEntity.Attributes; + _allowedRelationships = _contextEntity.Relationships; + _fieldExplorer = fieldExplorer; } - private bool InstanceOutputAttrsAreSpecified() + public ResourceDefinition(IResourceGraph graph) { - 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; + _allowedAttributes = _contextEntity.Attributes; + _allowedRelationships = _contextEntity.Relationships; + _contextEntity = graph.GetContextEntity(typeof(TResource)); } - /// - /// 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 }; "); - } + public List GetAllowedRelationships() => _allowedRelationships; + public List GetAllowedAttributes() => _allowedAttributes; /// /// Allows POST / PATCH requests to set the value of an @@ -95,38 +56,18 @@ protected List Remove(Expression> filter, List - protected virtual List OutputAttrs() => _contextEntity.Attributes; - - /// - /// 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. - /// - protected virtual List OutputAttrs(T instance) => _contextEntity.Attributes; - - public List GetOutputAttrs(object instance) - => _instanceAttrsAreSpecified == false - ? GetOutputAttrs() - : OutputAttrs(instance as T); - - private List GetOutputAttrs() + public void HideAttributes(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 attributesToHide = _fieldExplorer.GetAttributes(selector); + _allowedAttributes = _allowedAttributes.Except(attributesToHide).ToList(); + } + public void HideRelationships(Expression> selector) + { + var relationshipsToHide = _fieldExplorer.GetRelationships(selector); + _allowedRelationships = _allowedRelationships.Except(relationshipsToHide).ToList(); } + /// /// Define a set of custom query expressions that can be applied /// instead of the default query behavior. A common use-case for this @@ -165,29 +106,29 @@ private List GetOutputAttrs() public virtual QueryFilters GetQueryFilters() => 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 +136,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. @@ -236,11 +177,12 @@ public class QueryFilters : Dictionary, FilterQuery, return null; } + /// /// This is an alias type intended to simplify the implementation's /// method signature. /// See for usage details. /// - public class PropertySortOrder : List<(Expression>, SortDirection)> { } + public class PropertySortOrder : List<(Expression>, SortDirection)> { } } } diff --git a/src/JsonApiDotNetCore/QueryServices/Contracts/IFieldQueryService.cs b/src/JsonApiDotNetCore/QueryServices/Contracts/IFieldQueryService.cs new file mode 100644 index 0000000000..b9328c297f --- /dev/null +++ b/src/JsonApiDotNetCore/QueryServices/Contracts/IFieldQueryService.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Services +{ + public interface IFieldsQueryService + { + List Get(RelationshipAttribute relationship = null); + } + + public interface IInternalFieldsQueryService + { + void Register(AttrAttribute selected, RelationshipAttribute relationship = null); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/QueryServices/Contracts/IIncludedQueryService.cs b/src/JsonApiDotNetCore/QueryServices/Contracts/IIncludedQueryService.cs new file mode 100644 index 0000000000..8160c98739 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryServices/Contracts/IIncludedQueryService.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Services +{ + public interface IIncludedQueryService + { + List> Get(); + } + + public interface IInternalIncludedQueryService + { + void Register(List inclusionChain); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/QueryServices/Contracts/IPageQueryService.cs b/src/JsonApiDotNetCore/QueryServices/Contracts/IPageQueryService.cs new file mode 100644 index 0000000000..b9e6e727d8 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryServices/Contracts/IPageQueryService.cs @@ -0,0 +1,36 @@ +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; +using System; +using System.Collections.Generic; +using System.Text; + +namespace JsonApiDotNetCore.Managers.Contracts +{ + public interface IPageQueryService + { + /// + /// 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; } + /// + /// Are we even paginating + /// + bool IsPaginated { get; } + int TotalPages { get; } + + bool ShouldPaginate(); + } +} diff --git a/src/JsonApiDotNetCore/QueryServices/FieldQueryService.cs b/src/JsonApiDotNetCore/QueryServices/FieldQueryService.cs new file mode 100644 index 0000000000..1b5f4ed0e5 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryServices/FieldQueryService.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Services +{ + public class FieldsQueryService : IFieldsQueryService, IInternalFieldsQueryService + { + private List _selectedFields; + private readonly Dictionary> _selectedRelationshipFields; + + public FieldsQueryService() + { + _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 void Register(AttrAttribute selected, RelationshipAttribute relationship = null) + { + if (relationship == null) + { + _selectedFields = _selectedFields ?? new List(); + _selectedFields.Add(selected); + } + + if (!_selectedRelationshipFields.TryGetValue(relationship, out var fields)) + _selectedRelationshipFields.Add(relationship, fields = new List()); + + fields.Add(selected); + } + } +} diff --git a/src/JsonApiDotNetCore/QueryServices/IncludedQueryService.cs b/src/JsonApiDotNetCore/QueryServices/IncludedQueryService.cs new file mode 100644 index 0000000000..0b036f6303 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryServices/IncludedQueryService.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Services +{ + + public class IncludedQueryService : IIncludedQueryService, IInternalIncludedQueryService + { + private readonly List> _includedChains; + + public IncludedQueryService() + { + _includedChains = new List>(); + } + + public List> Get() + { + return _includedChains; + } + + public void Register(List chain) + { + _includedChains.Add(chain); + } + } +} diff --git a/src/JsonApiDotNetCore/QueryServices/PageQueryService.cs b/src/JsonApiDotNetCore/QueryServices/PageQueryService.cs new file mode 100644 index 0000000000..effe5d8564 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryServices/PageQueryService.cs @@ -0,0 +1,29 @@ +using System; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Managers.Contracts; + +namespace JsonApiDotNetCore.Internal +{ + public class PageQueryService : IPageQueryService + { + private IJsonApiOptions _options; + + public PageQueryService(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 bool IsPaginated => PageSize > 0; + public int TotalPages => (TotalRecords == null) ? -1 : (int)Math.Ceiling(decimal.Divide(TotalRecords.Value, PageSize)); + + public bool ShouldPaginate() + { + return !IsPaginated || ((CurrentPage == 1 || CurrentPage == 0) && TotalPages <= 0); + } + } +} 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/IRequestManager.cs b/src/JsonApiDotNetCore/RequestServices/Contracts/IRequestManager.cs new file mode 100644 index 0000000000..37dc6485dc --- /dev/null +++ b/src/JsonApiDotNetCore/RequestServices/Contracts/IRequestManager.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Http; + +namespace JsonApiDotNetCore.Managers.Contracts +{ + public interface IRequestContext : 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; } + QuerySet QuerySet { get; set; } + IQueryCollection FullQuerySet { get; set; } + + /// + /// If the request is on the `{id}/relationships/{relationshipName}` route + /// + bool IsRelationshipPath { get; set; } + /// + /// Sets the current context entity for this entire request + /// + /// + void SetRequestResource(ContextEntity contextEntityCurrent); + + ContextEntity GetRequestResource(); + /// + /// Which query params are filtered + /// + QueryParams DisabledQueryParams { get; set; } + bool IsBulkRequest { get; set; } + + } +} diff --git a/src/JsonApiDotNetCore/RequestServices/Contracts/IResourceGraphManager.cs b/src/JsonApiDotNetCore/RequestServices/Contracts/IResourceGraphManager.cs new file mode 100644 index 0000000000..3a58f3e2b2 --- /dev/null +++ b/src/JsonApiDotNetCore/RequestServices/Contracts/IResourceGraphManager.cs @@ -0,0 +1,11 @@ +using JsonApiDotNetCore.Internal; +using System; +using System.Collections.Generic; +using System.Text; + +namespace JsonApiDotNetCore.Managers.Contracts +{ + public interface IResourceGraphManager + { + } +} diff --git a/src/JsonApiDotNetCore/RequestServices/Contracts/IUpdatedFields.cs b/src/JsonApiDotNetCore/RequestServices/Contracts/IUpdatedFields.cs new file mode 100644 index 0000000000..c7deab88eb --- /dev/null +++ b/src/JsonApiDotNetCore/RequestServices/Contracts/IUpdatedFields.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Serialization +{ + public interface IUpdatedFields + { + List Attributes { get; set; } + List Relationships { get; set; } + } + + public class UpdatedFields: IUpdatedFields + { + public List Attributes { get; set; } = new List(); + public List Relationships { get; set; } = new List(); + } + +} diff --git a/src/JsonApiDotNetCore/RequestServices/RequestManager.cs b/src/JsonApiDotNetCore/RequestServices/RequestManager.cs new file mode 100644 index 0000000000..3d878fd77d --- /dev/null +++ b/src/JsonApiDotNetCore/RequestServices/RequestManager.cs @@ -0,0 +1,57 @@ +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Text; + +namespace JsonApiDotNetCore.Managers +{ + + class RequestContext : IRequestContext + { + private ContextEntity _contextEntity; + private IQueryParser _queryParser; + + public string BasePath { get; set; } + public List IncludedRelationships { get; set; } + public QuerySet QuerySet { get; set; } + public PageQueryService PageManager { get; set; } + public IQueryCollection FullQuerySet { get; set; } + public QueryParams DisabledQueryParams { get; set; } + public bool IsRelationshipPath { get; set; } + public Dictionary AttributesToUpdate { get; set; } + + public Dictionary RelationshipsToUpdate { get; set; } + + public bool IsBulkRequest { get; set; } = false; + + public List GetFields() + { + return QuerySet?.Fields; + } + + public List GetRelationships() + { + return QuerySet?.IncludedRelationships; + } + + /// s + /// The main resource of the request. + /// + /// + public ContextEntity GetRequestResource() + { + return _contextEntity; + } + + public void SetRequestResource(ContextEntity requestResource) + { + _contextEntity = requestResource; + } + } +} diff --git a/src/JsonApiDotNetCore/RequestServices/SerializableFields.cs b/src/JsonApiDotNetCore/RequestServices/SerializableFields.cs new file mode 100644 index 0000000000..2888840f5e --- /dev/null +++ b/src/JsonApiDotNetCore/RequestServices/SerializableFields.cs @@ -0,0 +1,58 @@ +using JsonApiDotNetCore.Internal.Contracts; +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Models +{ + public class SerializableFields : ISerializableFields + { + private readonly IContextEntityProvider _resourceContextProvider; + private readonly IServiceProvider _provider; + private readonly Dictionary _resourceDefinitionCache = new Dictionary(); + private readonly IExposedFieldExplorer _fieldExplorer; + + public SerializableFields(IExposedFieldExplorer fieldExplorer, + IContextEntityProvider resourceContextProvider, + IServiceProvider provider) + { + _fieldExplorer = fieldExplorer; + _resourceContextProvider = resourceContextProvider; + _provider = provider; + } + + public List GetAllowedAttributes(Type type) + { + var resourceDefinition = GetResourceDefinition(type); + if (resourceDefinition != null) + // The set of allowed attribrutes to be exposed was defined on the resource definition + return resourceDefinition.GetAllowedAttributes(); + + // The set of allowed attribrutes to be exposed was NOT defined on the resource definition: return all + return _fieldExplorer.GetAttributes(type); + } + + public List GetAllowedRelationships(Type type) + { + var resourceDefinition = GetResourceDefinition(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 _fieldExplorer.GetRelationships(type); + } + + private IResourceDefinition GetResourceDefinition(Type resourceType) + { + + var resourceDefinitionType = _resourceContextProvider.GetContextEntity(resourceType).ResourceType; + if (!_resourceDefinitionCache.TryGetValue(resourceDefinitionType, out IResourceDefinition resourceDefinition)) + { + resourceDefinition = _provider.GetService(resourceDefinitionType) as IResourceDefinition; + _resourceDefinitionCache.Add(resourceDefinitionType, resourceDefinition); + } + return resourceDefinition; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Contracts/IClientDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Contracts/IClientDeserializer.cs new file mode 100644 index 0000000000..465afc218d --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Contracts/IClientDeserializer.cs @@ -0,0 +1,10 @@ +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Serialization.Contracts +{ + public interface IClientDeserializer + { + DeserializedSingleResponse DeserializeSingle(string body) where TResource : class, IIdentifiable; + DeserializedListResponse DeserializeList(string body) where TResource : class, IIdentifiable; + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/Contracts/IIncludedRelationshipsBuilder.cs b/src/JsonApiDotNetCore/Serialization/Contracts/IIncludedRelationshipsBuilder.cs new file mode 100644 index 0000000000..c5889b0bd2 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Contracts/IIncludedRelationshipsBuilder.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Builders +{ + public interface IIncludedRelationshipsBuilder + { + List Build(); + void IncludeRelationshipChain(List inclusionChain, IIdentifiable rootEntity); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/Contracts/IJsonApiDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Contracts/IJsonApiDeserializer.cs new file mode 100644 index 0000000000..4246034a43 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Contracts/IJsonApiDeserializer.cs @@ -0,0 +1,7 @@ +namespace JsonApiDotNetCore.Serialization.Contracts +{ + public interface IJsonApiDeserializer + { + object Deserialize(string body); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Contracts/IJsonApiSerializer.cs b/src/JsonApiDotNetCore/Serialization/Contracts/IJsonApiSerializer.cs new file mode 100644 index 0000000000..b58c24f5fb --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Contracts/IJsonApiSerializer.cs @@ -0,0 +1,10 @@ +using System.Collections; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Builders +{ + public interface IJsonApiSerializer + { + string Serialize(object content); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/Contracts/ILinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Contracts/ILinkBuilder.cs new file mode 100644 index 0000000000..572389867c --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Contracts/ILinkBuilder.cs @@ -0,0 +1,24 @@ +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.Links; + +namespace JsonApiDotNetCore.Builders +{ + public interface ILinkBuilder + { + /// + /// Builds the links object that is included in the top-level of the document. + /// + TopLevelLinks GetTopLevelLinks(); + /// + /// 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/Builders/IMetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Contracts/IMetaBuilder.cs similarity index 53% rename from src/JsonApiDotNetCore/Builders/IMetaBuilder.cs rename to src/JsonApiDotNetCore/Serialization/Contracts/IMetaBuilder.cs index bf35b9d210..d563329b30 100644 --- a/src/JsonApiDotNetCore/Builders/IMetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Contracts/IMetaBuilder.cs @@ -1,11 +1,13 @@ +using System; using System.Collections.Generic; +using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Builders { - public interface IMetaBuilder + public interface IMetaBuilder where T : class, IIdentifiable { void Add(string key, object value); void Add(Dictionary values); - Dictionary Build(); + Dictionary GetMeta(); } } \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/Contracts/IServerSerializerFactory.cs b/src/JsonApiDotNetCore/Serialization/Contracts/IServerSerializerFactory.cs new file mode 100644 index 0000000000..d0aec078ee --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Contracts/IServerSerializerFactory.cs @@ -0,0 +1,7 @@ +namespace JsonApiDotNetCore.Builders +{ + public interface IJsonApiSerializerFactory + { + IJsonApiSerializer GetSerializer(); + } +} \ No newline at end of file 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/Deserializer/ClientDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Deserializer/ClientDeserializer.cs new file mode 100644 index 0000000000..f1571a27e2 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Deserializer/ClientDeserializer.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization.Contracts; + +namespace JsonApiDotNetCore.Serialization +{ + public class ClientDeserializer : DocumentParser, IClientDeserializer + { + public ClientDeserializer(IContextEntityProvider 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 + }; + } + + protected override void AfterProcessField(IIdentifiable entity, IResourceField field, RelationshipData data = null) + { + if (field is AttrAttribute) + return; + + // can't provide any more data other than the rios since it is not contained in the included section + if (_document.Included == null || _document.Included.Count == 0) + return; + + if (field is HasOneAttribute hasOneAttr) + { + var rio = data.SingleData; + if (rio == null) + hasOneAttr.SetValue(entity, null); + else + hasOneAttr.SetValue(entity, GetIncludedRelationship(hasOneAttr, rio)); + } + else if (field is HasManyAttribute hasManyAttr) + { + var values = TypeHelper.CreateListFor(hasManyAttr.DependentType); + foreach (var rio in data.ManyData) + values.Add(GetIncludedRelationship(hasManyAttr, rio)); + + hasManyAttr.SetValue(entity, values); + } + } + + private IIdentifiable GetIncludedRelationship(RelationshipAttribute relationshipAttr, ResourceIdentifierObject relatedResourceIdentifier) + { + var relatedInstance = relationshipAttr.DependentType.New(); + relatedInstance.StringId = relatedResourceIdentifier.Id; + + var includedResource = GetLinkedResource(relatedResourceIdentifier); + if (includedResource == null) + return relatedInstance; + + var contextEntity = _provider.GetContextEntity(relatedResourceIdentifier.Type); + if (contextEntity == null) + throw new InvalidOperationException($"Included type '{relationshipAttr.DependentType}' is not a registered json:api resource."); + + SetAttributes(relatedInstance, includedResource.Attributes, contextEntity.Attributes); + SetRelationships(relatedInstance, includedResource.Relationships, contextEntity.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/Deserializer/DeserializedResponse.cs b/src/JsonApiDotNetCore/Serialization/Deserializer/DeserializedResponse.cs new file mode 100644 index 0000000000..0c3ac99401 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Deserializer/DeserializedResponse.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.Links; + +namespace JsonApiDotNetCore.Serialization +{ + /// TODO: Currently and + /// information is ignored by the serializer. This is considered not mission critical for now, and therefore out of scope. + public class DeserializedResponseBase + { + public TopLevelLinks Links { get; internal set; } + public Dictionary Meta { get; internal set; } + public object Errors { get; internal set; } + public object JsonApi { get; internal set; } + } + + public class DeserializedSingleResponse : DeserializedResponseBase where TResource : class, IIdentifiable + { + public TResource Data { get; internal set; } + } + + public class DeserializedListResponse : DeserializedResponseBase where TResource : class, IIdentifiable + { + public List Data { get; internal set; } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Deserializer/DocumentParser.cs b/src/JsonApiDotNetCore/Serialization/Deserializer/DocumentParser.cs new file mode 100644 index 0000000000..e06263edb0 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Deserializer/DocumentParser.cs @@ -0,0 +1,215 @@ +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 +{ + /// + /// Base class for deserialization. + /// + public abstract class DocumentParser + { + protected Document _document; + protected readonly IContextEntityProvider _provider; + + protected DocumentParser(IContextEntityProvider provider) + { + _provider = provider; + } + + protected abstract void AfterProcessField(IIdentifiable entity, IResourceField field, RelationshipData 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(DocumentToObject).ToList(); + } + else + { + if (_document.SingleData == null) return null; + return DocumentToObject(_document.SingleData); + } + } + + 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; + } + + protected IIdentifiable SetRelationships(IIdentifiable entity, Dictionary relationships, List relationshipAttributes) + { + if (relationships == null || relationships.Count == 0) + return entity; + + var entityProperties = entity.GetType().GetProperties(); + foreach (var attr in relationshipAttributes) + { + if (attr is HasOneAttribute hasOne) + SetHasOneRelationship(entity, entityProperties, (HasOneAttribute)attr, relationships); + else + SetHasManyRelationship(entity, (HasManyAttribute)attr, relationships); + + } + return entity; + } + + private JToken LoadJToken(string body) + { + JToken jToken; + using (JsonReader jsonReader = new JsonTextReader(new StringReader(body))) + { + jToken = JToken.Load(jsonReader); + } + return jToken; + } + + private IIdentifiable DocumentToObject(ResourceObject data) + { + var contextEntity = _provider.GetContextEntity(data.Type); + if (contextEntity == 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(contextEntity.EntityType); + + entity = SetAttributes(entity, data.Attributes, contextEntity.Attributes); + entity = SetRelationships(entity, data.Relationships, contextEntity.Relationships); + + if (data.Id != null) + entity.StringId = data.Id?.ToString(); + + 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); + //return obj.ToObject(targetType, _jsonSerializer); + } + + private object SetHasOneRelationship(IIdentifiable entity, + PropertyInfo[] entityProperties, + HasOneAttribute attr, + Dictionary relationships) + { + if (relationships.TryGetValue(attr.PublicRelationshipName, out RelationshipData relationshipData) == false) + return entity; + + 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 no FK from the current entity pointing to the related object, + /// i.e. means we're populating the relationship from the principal side. + SetPrincipalSide(entity, attr, relatedId); + } + else + { + /// there is a FK from the current entity pointing to the related object, + /// i.e. we're populating the relationship from the dependent side. + SetDependentSide(entity, foreignKeyProperty, attr, relatedId); + } + + AfterProcessField(entity, attr, relationshipData); + + return entity; + } + + private void SetDependentSide(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); + } + + private void SetPrincipalSide(IIdentifiable entity, HasOneAttribute attr, string relatedId) + { + if (relatedId == null) + { + attr.SetValue(entity, null); + } + else + { + var relatedInstance = attr.DependentType.New(); + relatedInstance.StringId = relatedId; + attr.SetValue(entity, relatedInstance); + } + } + + + private object SetHasManyRelationship(IIdentifiable entity, + HasManyAttribute attr, + Dictionary relationships) + { + if (relationships.TryGetValue(attr.PublicRelationshipName, out RelationshipData relationshipData)) + { + if (!relationshipData.IsManyData) + return entity; + + var relatedResources = relationshipData.ManyData.Select(rio => + { + var relatedInstance = attr.DependentType.New(); + relatedInstance.StringId = rio.Id; + return relatedInstance; + }); + + var convertedCollection = TypeHelper.ConvertCollection(relatedResources, attr.DependentType); + attr.SetValue(entity, convertedCollection); + AfterProcessField(entity, attr, relationshipData); + } + + return entity; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/Deserializer/OperationsDeserializer.cs similarity index 72% rename from src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs rename to src/JsonApiDotNetCore/Serialization/Deserializer/OperationsDeserializer.cs index c11c011852..cfaaf8c2e4 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Deserializer/OperationsDeserializer.cs @@ -5,22 +5,28 @@ using System.Reflection; using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Generics; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Models.Operations; -using JsonApiDotNetCore.Services; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace JsonApiDotNetCore.Serialization { - public class JsonApiDeSerializer : IJsonApiDeSerializer + + public class OperationsDeserializer : IOperationsDeserializer { - private readonly IJsonApiContext _jsonApiContext; + private readonly IUpdatedFields _updatedFieldsManager; + private readonly IResourceGraph _resourceGraph; + private readonly JsonSerializer _jsonSerializer; - public JsonApiDeSerializer(IJsonApiContext jsonApiContext) + public OperationsDeserializer(IUpdatedFields updatedFieldsManager, + IResourceGraph resourceGraph, + IJsonApiSerializerSettings serializerSettings) { - _jsonApiContext = jsonApiContext; + _updatedFieldsManager = updatedFieldsManager; + _resourceGraph = resourceGraph; + _jsonSerializer = JsonSerializer.Create(serializerSettings.GetSettings()); } public object Deserialize(string requestBody) @@ -33,24 +39,11 @@ public object Deserialize(string 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(); + var operations = JsonConvert.DeserializeObject(requestBody); + if (operations == null) + throw new JsonApiException(400, "Failed to deserialize operations request."); - _jsonApiContext.DocumentMeta = document.Meta; - var entity = DocumentToObject(document.Data, document.Included); - return entity; + return operations; } catch (JsonApiException) { @@ -62,61 +55,21 @@ public object Deserialize(string requestBody) } } - 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, + var contextEntity = _resourceGraph.GetContextEntity(data.Type?.ToString()); + if (contextEntity == 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 = Activator.CreateInstance(contextEntity.EntityType); @@ -145,12 +98,7 @@ private object SetEntityAttributes( 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; + _updatedFieldsManager.Attributes.Add(attr); } } @@ -168,7 +116,7 @@ private object ConvertAttrValue(object newValue, Type targetType) private object DeserializeComplexType(JContainer obj, Type targetType) { - return obj.ToObject(targetType, JsonSerializer.Create(_jsonApiContext.Options.SerializerSettings)); + return obj.ToObject(targetType, _jsonSerializer); } private object SetRelationships( @@ -204,7 +152,7 @@ private object SetHasOneRelationship(object entity, if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData) == false) return entity; - var rio = (ResourceIdentifierObject)relationshipData.ExposedData; + var rio = (ResourceIdentifierObject)relationshipData.Data; var foreignKey = attr.IdentifiablePropertyName; var foreignKeyProperty = entityProperties.FirstOrDefault(p => p.Name == foreignKey); @@ -219,8 +167,8 @@ private object SetHasOneRelationship(object entity, { var navigationPropertyValue = attr.GetValue(entity); - var resourceGraphEntity = _jsonApiContext.ResourceGraph.GetContextEntity(attr.DependentType); - if(navigationPropertyValue != null && resourceGraphEntity != null) + var resourceGraphEntity = _resourceGraph.GetContextEntity(attr.DependentType); + if (navigationPropertyValue != null && resourceGraphEntity != null) { var includedResource = included.SingleOrDefault(r => r.Type == rio.Type && r.Id == rio.Id); @@ -253,7 +201,7 @@ private void SetHasOneForeignKeyValue(object entity, HasOneAttribute hasOneAttr, /// 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); + if (convertedValue == null) _updatedFieldsManager.Relationships.Add(hasOneAttr); } } @@ -278,7 +226,7 @@ private void SetHasOneNavigationPropertyValue(object entity, HasOneAttribute has /// 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); + _updatedFieldsManager.Relationships.Add(hasOneAttr); } } @@ -293,7 +241,7 @@ private object SetHasManyRelationship(object entity, if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData)) { - if (relationshipData.IsHasMany == false || relationshipData.ManyData == null) + if (relationshipData.IsManyData == false) return entity; var relatedResources = relationshipData.ManyData.Select(r => @@ -305,12 +253,7 @@ private object SetHasManyRelationship(object entity, 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); + _updatedFieldsManager.Relationships.Add(attr); } return entity; @@ -330,7 +273,7 @@ private IIdentifiable GetIncludedRelationship(ResourceIdentifierObject relatedRe if (includedResource == null) return relatedInstance; - var contextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationshipAttr.DependentType); + var contextEntity = _resourceGraph.GetContextEntity(relationshipAttr.DependentType); if (contextEntity == null) throw new JsonApiException(400, $"Included type '{relationshipAttr.DependentType}' is not a registered json:api resource."); @@ -352,4 +295,10 @@ private ResourceObject GetLinkedResource(ResourceIdentifierObject relatedResourc } } } -} + + public interface IOperationsDeserializer + { + object Deserialize(string body); + object DocumentToObject(ResourceObject data, List included = null); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/Deserializer/ServerDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Deserializer/ServerDeserializer.cs new file mode 100644 index 0000000000..39549b732b --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Deserializer/ServerDeserializer.cs @@ -0,0 +1,36 @@ +using System; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization.Contracts; + +namespace JsonApiDotNetCore.Serialization +{ + public class ServerDeserializer : DocumentParser, IJsonApiDeserializer + { + protected readonly IUpdatedFields _updatedFields; + + public ServerDeserializer(IResourceGraph resourceGraph, + IUpdatedFields updatedFields) : base(resourceGraph) + { + _updatedFields = updatedFields; + } + + public new object Deserialize(string body) + { + return base.Deserialize(body); + } + + protected override void AfterProcessField(IIdentifiable entity, IResourceField field, RelationshipData data = null) + { + if (field is AttrAttribute attr) + { + if (!attr.IsImmutable) + _updatedFields.Attributes.Add(attr); + else + throw new InvalidOperationException($"Attribute {attr.PublicAttributeName} is immutable and therefore cannot be updated."); + } + else if (field is RelationshipAttribute relationship) + _updatedFields.Relationships.Add(relationship); + } + } +} 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/IJsonApiSerializerSettings.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiSerializerSettings.cs new file mode 100644 index 0000000000..364eddb3df --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiSerializerSettings.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.Serialization +{ + public interface IJsonApiSerializerSettings + { + JsonSerializerSettings GetSettings(); + } +} 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/JsonApiSerializerSettings.cs b/src/JsonApiDotNetCore/Serialization/JsonApiSerializerSettings.cs new file mode 100644 index 0000000000..a0f56bb5f8 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/JsonApiSerializerSettings.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.Serialization +{ + public class JsonApiSerializerSettings : IJsonApiSerializerSettings + { + public JsonSerializerSettings GetSettings() + { + return new JsonSerializerSettings() + { + NullValueHandling = NullValueHandling.Ignore, + DateParseHandling = DateParseHandling.None + }; + } + + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Serializer/ClientSerializer.cs b/src/JsonApiDotNetCore/Serialization/Serializer/ClientSerializer.cs new file mode 100644 index 0000000000..4203976755 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Serializer/ClientSerializer.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq.Expressions; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Builders +{ + public class ClientSerializer : DocumentBuilder + { + private readonly Dictionary> _attributesToSerializeCache = new Dictionary>(); + private readonly Dictionary> _relationshipsToSerializeCache = new Dictionary>(); + private Type _currentTargetedResource; + private readonly IExposedFieldExplorer _fieldExplorer; + public ClientSerializer(IExposedFieldExplorer fieldExplorer, + IContextEntityProvider provider, + IResourceGraph resourceGraph) : base(resourceGraph, provider) + { + _fieldExplorer = fieldExplorer; + } + + /// + /// Creates and serializes a document for a single intance of a resource. + /// + /// Entity to serialize + /// The serialized content + public string Serialize(IIdentifiable entity) + { + if (entity == null) + return GetStringOutput(base.Build(entity)); + + _currentTargetedResource = entity?.GetType(); + var attributes = GetAttributesToSerialize(entity); + var relationships = GetRelationshipsToSerialize(entity); + var document = base.Build(entity, attributes, relationships); + _currentTargetedResource = null; + return GetStringOutput(document); + + } + + /// + /// Creates and serializes a document for for a list of entities of one resource. + /// + /// Entities to serialize + /// The serialized content + public string Serialize(IEnumerable entities) + { + IIdentifiable entity = null; + foreach (IIdentifiable item in entities) + { + entity = item; + break; + } + if (entity == null) + return GetStringOutput(base.Build(entities)); + + _currentTargetedResource = entity?.GetType(); + var attributes = GetAttributesToSerialize(entity); + var relationships = GetRelationshipsToSerialize(entity); + var document = base.Build(entities, attributes, relationships); + _currentTargetedResource = null; + return GetStringOutput(document); + } + + /// + /// Sets the s to serialize for resources of type . + /// If no s are specified, by default all attributes are included in the serialization result. + /// + /// + /// + public void SetAttributesToSerialize(Expression> filter) where T : class, IIdentifiable + { + var allowedAttributes = _fieldExplorer.GetAttributes(filter); + _attributesToSerializeCache[typeof(T)] = allowedAttributes; + } + + /// + /// Sets the s to serialize for resources of type . + /// If no s are specified, by default no relationships are included in the serialization result. + /// + /// + /// + public void SetRelationshipsToSerialize(Expression> filter) where T : class, IIdentifiable + { + var allowedRelationships = _fieldExplorer.GetRelationships(filter); + _relationshipsToSerializeCache[typeof(T)] = 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. + /// + /// Entity to be serialized + /// List of allowed attributes in the serialized result. + 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 _fieldExplorer.GetAttributes(resourceType); + + return attributes; + } + + /// + /// By default, the client serializer does not include any relationships + /// for entities in the primary data unless explicitly included using + /// . + /// + /// Entity to be serialized + /// List of allowed relationships in the serialized result. + 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/Serializer/DocumentBuilder.cs b/src/JsonApiDotNetCore/Serialization/Serializer/DocumentBuilder.cs new file mode 100644 index 0000000000..6aad0c246e --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Serializer/DocumentBuilder.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.Builders +{ + public abstract class DocumentBuilder : ResourceObjectBuilder + { + protected DocumentBuilder(IResourceGraph resourceGraph, IContextEntityProvider provider) : base(resourceGraph, provider) { } + protected Document Build(IIdentifiable entity, List attributes = null, List relationships = null) + { + if (entity == null) + return new Document(); + + return new Document { Data = BuildResourceObject(entity, attributes, relationships) }; + } + + protected Document Build(IEnumerable entities, List attributes = null, List relationships = null) + { + var data = new List(); + foreach (IIdentifiable entity in entities) + data.Add(BuildResourceObject(entity, attributes, relationships)); + + return new Document { Data = data }; + } + + protected string GetStringOutput(Document document) + { + //var settings = new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore }; + return JsonConvert.SerializeObject(document); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Serializer/IncludedRelationshipsBuilder.cs b/src/JsonApiDotNetCore/Serialization/Serializer/IncludedRelationshipsBuilder.cs new file mode 100644 index 0000000000..32907b620c --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Serializer/IncludedRelationshipsBuilder.cs @@ -0,0 +1,112 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Builders +{ + + public class IncludedRelationshipsBuilder : ResourceObjectBuilder, IIncludedRelationshipsBuilder + { + private readonly HashSet _included; + private readonly ISerializableFields _serializableFields; + private readonly ILinkBuilder _linkBuilder; + + public IncludedRelationshipsBuilder(ISerializableFields serializableFields, + ILinkBuilder linkBuilder, + IResourceGraph resourceGraph, + IContextEntityProvider provider) : base(resourceGraph, provider) + { + _included = new HashSet(new ResourceObjectComparer()); + _serializableFields = serializableFields; + _linkBuilder = linkBuilder; + } + + public List Build() + { + if (_included.Any()) + { + foreach (var resourceObject in _included) + { + if (resourceObject.Relationships != null) + { + 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 + /// in the documents primary data. + var relationship = inclusionChain.First(); + var chainRemainder = ShiftChain(inclusionChain); + var related = _resourceGraph.GetRelationshipValue(rootEntity, relationship); + + 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) + { + var resourceObject = GetOrBuildResourceObject(parent, originRelationship); + if (!inclusionChain.Any()) + return; + + var nextRelationship = inclusionChain.First(); + var chainRemainder = inclusionChain.ToList(); + chainRemainder.RemoveAt(0); + var relationshipData = base.GetRelationshipData(nextRelationship, parent); + resourceObject.Relationships[nextRelationship.PublicRelationshipName] = relationshipData; + if (relationshipData.HasData) + { + var related = _resourceGraph.GetRelationshipValue(parent, nextRelationship); + ProcessChain(nextRelationship, related, chainRemainder); + } + } + + private List ShiftChain(List chain) + { + var chainRemainder = chain.ToList(); + chainRemainder.RemoveAt(0); + return chainRemainder; + } + + protected override RelationshipData GetRelationshipData(RelationshipAttribute relationship, IIdentifiable entity) + { + return new RelationshipData { Links = _linkBuilder.GetRelationshipLinks(relationship, entity) }; + } + + private ResourceObject GetOrBuildResourceObject(IIdentifiable parent, RelationshipAttribute attr) + { + var type = parent.GetType(); + var resourceName = _provider.GetContextEntity(type).EntityName; + var entry = _included.SingleOrDefault(ro => ro.Type == resourceName && ro.Id == parent.StringId); + if (entry == null) + { + entry = BuildResourceObject(parent, _serializableFields.GetAllowedAttributes(type), _serializableFields.GetAllowedRelationships(type)); + _included.Add(entry); + } + return entry; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Serializer/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Serializer/LinkBuilder.cs new file mode 100644 index 0000000000..1a70101298 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Serializer/LinkBuilder.cs @@ -0,0 +1,152 @@ +using System; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.Links; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Builders +{ + public class LinkBuilder : ILinkBuilder + { + private readonly IRequestContext _requestManager; + private readonly IGlobalLinksConfiguration _options; + private readonly IPageQueryService _pageManager; + private readonly ContextEntity _requestResourceContext; + private readonly IContextEntityProvider _provider; + + public LinkBuilder(IGlobalLinksConfiguration options, + IRequestContext requestManager, + IPageQueryService pageManager, + IContextEntityProvider provider) + { + _options = options; + _requestManager = requestManager; + _pageManager = pageManager; + _provider = provider; + _requestResourceContext = _requestManager.GetRequestResource(); + } + + /// + public TopLevelLinks GetTopLevelLinks() + { + TopLevelLinks topLevelLinks = null; + if (ShouldAddTopLevelLink(Link.Self)) + topLevelLinks = new TopLevelLinks { Self = GetSelfTopLevelLink(_requestResourceContext.EntityName) }; + + if (ShouldAddTopLevelLink(Link.Paging)) + SetPageLinks(ref topLevelLinks); + + return topLevelLinks; + } + + /// + public ResourceLinks GetResourceLinks(string resourceName, string id) + { + var resourceContext = _provider.GetContextEntity(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.GetContextEntity(parent.GetType()); + var childNavigation = relationship.PublicRelationshipName; + RelationshipLinks links = null; + if (ShouldAddRelationshipLink(parentResourceContext, relationship, Link.Related)) + links = new RelationshipLinks { Related = GetRelatedRelationshipLink(parentResourceContext.EntityName, parent.StringId, childNavigation) }; + + if (ShouldAddRelationshipLink(parentResourceContext, relationship, Link.Self)) + { + links = links ?? new RelationshipLinks(); + links.Self = GetSelfRelationshipLink(parentResourceContext.EntityName, parent.StringId, childNavigation); + } + + return links; + } + + private void SetPageLinks(ref TopLevelLinks links) + { + if (!_pageManager.ShouldPaginate()) + return; + + links = links ?? new TopLevelLinks(); + + if (_pageManager.CurrentPage > 1) + { + links.First = GetPageLink(1, _pageManager.PageSize); + links.Prev = GetPageLink(_pageManager.CurrentPage - 1, _pageManager.PageSize); + } + + + if (_pageManager.CurrentPage < _pageManager.TotalPages) + links.Next = GetPageLink(_pageManager.CurrentPage + 1, _pageManager.PageSize); + + + if (_pageManager.TotalPages > 0) + links.Last = GetPageLink(_pageManager.TotalPages, _pageManager.PageSize); + } + + private string GetSelfTopLevelLink(string resourceName) + { + return $"{GetBasePath()}/{resourceName}"; + } + + 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}"; + } + + private string GetPageLink(int pageOffset, int pageSize) + { + var filterQueryComposer = new QueryComposer(); + var filters = filterQueryComposer.Compose(_requestManager); + return $"{GetBasePath()}/{_requestResourceContext.EntityName}?page[size]={pageSize}&page[number]={pageOffset}{filters}"; + } + + private bool ShouldAddTopLevelLink(Link link) + { + if (_requestResourceContext.TopLevelLinks != Link.NotConfigured) + return _requestResourceContext.TopLevelLinks.HasFlag(link); + return _options.TopLevelLinks.HasFlag(link); + } + + private bool ShouldAddResourceLink(ContextEntity resourceContext, Link link) + { + if (resourceContext.ResourceLinks != Link.NotConfigured) + return resourceContext.ResourceLinks.HasFlag(link); + return _options.ResourceLinks.HasFlag(link); + } + + private bool ShouldAddRelationshipLink(ContextEntity 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); + } + + private string GetBasePath() + { + if (_options.RelativeLinks) + return string.Empty; + return _requestManager.BasePath; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Serializer/MetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Serializer/MetaBuilder.cs new file mode 100644 index 0000000000..643244269d --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Serializer/MetaBuilder.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Builders +{ + public class MetaBuilder : IMetaBuilder where T : class, IIdentifiable + { + private Dictionary _meta = new Dictionary(); + private readonly IPageQueryService _pageManager; + private readonly IJsonApiOptions _options; + private readonly IRequestMeta _requestMeta; + private readonly IHasMeta _resourceMeta; + + public MetaBuilder(IPageQueryService pageManager, + IJsonApiOptions options, + IRequestMeta requestMeta = null, + ResourceDefinition resourceDefinition = null) + { + _pageManager = pageManager; + _options = options; + _requestMeta = requestMeta; + _resourceMeta = resourceDefinition as IHasMeta; + } + + 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 GetMeta() + { + if (_options.IncludeTotalRecordCount && _pageManager.TotalRecords != null) + _meta.Add("total-records", _pageManager.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/Serializer/ResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Serializer/ResourceObjectBuilder.cs new file mode 100644 index 0000000000..adfa84009a --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Serializer/ResourceObjectBuilder.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Builders +{ + public abstract class ResourceObjectBuilder + { + protected readonly IResourceGraph _resourceGraph; + protected readonly IContextEntityProvider _provider; + private const string _identifiablePropertyName = nameof(Identifiable.Id); + + protected ResourceObjectBuilder(IResourceGraph resourceGraph, IContextEntityProvider provider) + { + _resourceGraph = resourceGraph; + _provider = provider; + } + + protected ResourceObject BuildResourceObject(IIdentifiable entity, IEnumerable attrs = null, IEnumerable rels = null) + { + var resourceContext = _provider.GetContextEntity(entity.GetType()); + + // populating the top-level "type" and "id" members. + var ro = new ResourceObject { Type = resourceContext.EntityName, Id = entity.StringId.NullIfEmpty() }; + + // populating the top-level "attribute" member, if any + if (attrs != null) + { + // never include "id" as an attribute + attrs = attrs.Where(attr => attr.InternalAttributeName != _identifiablePropertyName); + if (attrs.Any()) + { + ro.Attributes = new Dictionary(); + foreach (var attr in attrs) + ro.Attributes.Add(attr.PublicAttributeName, attr.GetValue(entity)); + } + } + + // populating top-level "relationship" member, if any + if (rels != null && rels.Any()) + { + foreach (var rel in rels) + { + var relData = GetRelationshipData(rel, entity); + if (relData != null) + (ro.Relationships = ro.Relationships ?? new Dictionary()).Add(rel.PublicRelationshipName, relData); + } + } + + return ro; + } + + protected ResourceIdentifierObject GetRelatedResourceLinkage(HasOneAttribute attr, IIdentifiable entity) + { + var relatedEntity = (IIdentifiable)_resourceGraph.GetRelationshipValue(entity, attr); + if (relatedEntity == null && IsRequiredToOneRelationship(attr, 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 CreateResourceIdentifier(relatedEntity); + + return null; + } + + protected List GetRelatedResourceLinkage(HasManyAttribute attr, IIdentifiable entity) + { + var relatedEntities = (IEnumerable)_resourceGraph.GetRelationshipValue(entity, attr); + var manyData = new List(); + if (relatedEntities != null) + foreach (IIdentifiable relatedEntity in relatedEntities) + manyData.Add(CreateResourceIdentifier(relatedEntity)); + + return manyData; + } + + /// + /// 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. + /// + protected virtual RelationshipData GetRelationshipData(RelationshipAttribute relationship, IIdentifiable entity) + { + if (relationship is HasOneAttribute hasOne) + return new RelationshipData { Data = GetRelatedResourceLinkage(hasOne, entity) }; + + return new RelationshipData { Data = GetRelatedResourceLinkage((HasManyAttribute)relationship, entity) }; + } + + /// + /// Creates a from . + /// + private ResourceIdentifierObject CreateResourceIdentifier(IIdentifiable entity) + { + var resourceName = _provider.GetContextEntity(entity.GetType()).EntityName; + 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; + } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/Serializer/ServerSerializer.cs b/src/JsonApiDotNetCore/Serialization/Serializer/ServerSerializer.cs new file mode 100644 index 0000000000..61b29dd516 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Serializer/ServerSerializer.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace JsonApiDotNetCore.Builders +{ + + public class ServerSerializerFactory : IJsonApiSerializerFactory + { + private readonly IRequestContext _requestManager; + private readonly IServiceProvider _provider; + + public ServerSerializerFactory(IRequestContext requestManager, IServiceProvider provider) + { + _requestManager = requestManager; + _provider = provider; + } + public IJsonApiSerializer GetSerializer() + { + var serializerType = typeof(ServerSerializer<>).MakeGenericType(_requestManager.GetRequestResource().EntityType); + return (IJsonApiSerializer)_provider.GetRequiredService(serializerType); + } + } + + public class ServerSerializer : DocumentBuilder, IJsonApiSerializer where T : class, IIdentifiable + { + private readonly Dictionary> _attributesToSerializeCache = new Dictionary>(); + private readonly Dictionary> _relationshipsToSerializeCache = new Dictionary>(); + private readonly IIncludedQueryService _includedQuery; + private readonly IFieldsQueryService _fieldQuery; + private readonly ISerializableFields _serializableFields; + private readonly IMetaBuilder _metaBuilder; + private readonly Type _requestResourceType; + private readonly ILinkBuilder _linkBuilder; + private readonly IIncludedRelationshipsBuilder _includedBuilder; + + public ServerSerializer( + IMetaBuilder metaBuilder, + ILinkBuilder linkBuilder, + IIncludedRelationshipsBuilder includedBuilder, + ISerializableFields serializableFields, + IIncludedQueryService includedQuery, + IFieldsQueryService fieldQuery, + IResourceGraph resourceGraph, IContextEntityProvider provider) : base(resourceGraph, provider) + { + _includedQuery = includedQuery; + _fieldQuery = fieldQuery; + _serializableFields = serializableFields; + _linkBuilder = linkBuilder; + _metaBuilder = metaBuilder; + _includedBuilder = includedBuilder; + _requestResourceType = typeof(T); + } + + public string Serialize(object content) + { + if (content is IEnumerable entities) + return SerializeMany(entities); + return SerializeSingle((IIdentifiable)content); + } + + internal string SerializeSingle(IIdentifiable entity) + { + var attributes = GetAttributesToSerialize(_requestResourceType); + var relationships = GetRelationshipsToSerialize(_requestResourceType); + var document = Build(entity, attributes, relationships); + var resourceObject = document.SingleData; + if (resourceObject != null) resourceObject.Links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); + AddTopLevelObjects(document); + return GetStringOutput(document); + } + + internal string SerializeMany(IEnumerable entities) + { + var attributes = GetAttributesToSerialize(_requestResourceType); + var relationships = GetRelationshipsToSerialize(_requestResourceType); + 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 GetStringOutput(document); + } + + /// + /// Gets the list of attributes to serialize for the given . + /// Depending on if instance-dependent attribute hiding was implemented in the corresponding + /// , the server serializer caches the output list of attributes + /// or recalculates it for every instance. 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 = _serializableFields.GetAllowedAttributes(resourceType); + var fields = _fieldQuery.Get(); + if (fields != null) + // from the allowed attributes, select the ones flagged by sparse field selection. + allowedAttributes = allowedAttributes.Where(attr => !fields.Contains(attr)).ToList(); + + // 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 = _serializableFields.GetAllowedRelationships(resourceType); + _relationshipsToSerializeCache.Add(resourceType, allowedRelations); + return allowedRelations; + + } + + /// + /// 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 RelationshipData GetRelationshipData(RelationshipAttribute relationship, IIdentifiable entity) + { + RelationshipData relationshipData = null; + /// if the relationship is included, populate the "data" field. + if (ShouldInclude(relationship, out var relationshipChain)) + { + relationshipData = base.GetRelationshipData(relationship, entity); + if (relationshipData.HasData) + _includedBuilder.IncludeRelationshipChain(relationshipChain, entity); + } + + var links = _linkBuilder.GetRelationshipLinks(relationship, entity); + /// if links relationshiplinks should be built for this entry, populate the "links" field. + if (links != null) + { + relationshipData = relationshipData ?? new RelationshipData(); + relationshipData.Links = links; + } + + /// if neither "links" nor "data" was popupated, return null, which will omit this entry from the output. + return relationshipData; + } + + private void AddTopLevelObjects(Document document) + { + document.Links = _linkBuilder.GetTopLevelLinks(); + document.Meta = _metaBuilder.GetMeta(); + document.Included = _includedBuilder.Build(); + } + + private bool ShouldInclude(RelationshipAttribute relationship, out List inclusionChain) + { + inclusionChain = _includedQuery.Get()?.SingleOrDefault(l => l.First().Equals(relationship)); + if (inclusionChain == null) + return false; + return true; + } + } +} 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/EntityResourceService.cs b/src/JsonApiDotNetCore/Services/EntityResourceService.cs index c889af68b8..26aebf5637 100644 --- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs +++ b/src/JsonApiDotNetCore/Services/EntityResourceService.cs @@ -1,5 +1,7 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Data; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Hooks; using Microsoft.Extensions.Logging; @@ -7,57 +9,49 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Serialization; 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) - { } - } - + /// + /// Entity mapping class + /// + /// + /// + /// public class EntityResourceService : IResourceService where TResource : class, IIdentifiable where TEntity : class, IIdentifiable { - private readonly IJsonApiContext _jsonApiContext; - private readonly IEntityRepository _entities; + private readonly IPageQueryService _pageManager; + private readonly IRequestContext _requestManager; + private readonly IJsonApiOptions _options; + private readonly IUpdatedFields _updatedFields; + private readonly IResourceGraph _resourceGraph; + private readonly IEntityRepository _repository; 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) + IEntityRepository repository, + IJsonApiOptions options, + IUpdatedFields updatedFields, + IRequestContext requestManager, + IPageQueryService pageManager, + IResourceGraph resourceGraph, + IResourceHookExecutor hookExecutor = null, + IResourceMapper mapper = null, + ILoggerFactory loggerFactory = null) { - _jsonApiContext = jsonApiContext; - _entities = entityRepository; - + _requestManager = requestManager; + _pageManager = pageManager; + _options = options; + _updatedFields = updatedFields; + _resourceGraph = resourceGraph; + _repository = repository; if (mapper == null && typeof(TResource) != typeof(TEntity)) { throw new InvalidOperationException("Resource and Entity types are NOT the same. Please provide a mapper."); @@ -67,41 +61,18 @@ public EntityResourceService( _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); + entity = await _repository.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) + if (_repository is IEntityFrameworkRepository efRepository) efRepository.DetachRelationshipPointers(entity); entity = await GetWithRelationshipsAsync(entity.Id); @@ -119,25 +90,24 @@ 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); + 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 entities = _entities.Get(); + var entities = _repository.Get(); entities = ApplySortAndFilterQuery(entities); if (ShouldIncludeRelationships()) - entities = IncludeRelationships(entities, _jsonApiContext.QuerySet.IncludedRelationships); - - if (_jsonApiContext.Options.IncludeTotalRecordCount) - _jsonApiContext.PageManager.TotalRecords = await _entities.CountAsync(entities); + entities = IncludeRelationships(entities, _requestManager.QuerySet.IncludedRelationships); + + if (_options.IncludeTotalRecordCount) + _pageManager.TotalRecords = await _repository.CountAsync(entities); - entities = _entities.Select(entities, _jsonApiContext.QuerySet?.Fields); + entities = _repository.Select(entities, _requestManager.QuerySet?.Fields); if (!IsNull(_hookExecutor, entities)) { @@ -146,8 +116,8 @@ public virtual async Task> GetAsync() entities = _hookExecutor.OnReturn(result, ResourcePipeline.Get).AsQueryable(); } - if (_jsonApiContext.Options.IncludeTotalRecordCount) - _jsonApiContext.PageManager.TotalRecords = await _entities.CountAsync(entities); + if (_options.IncludeTotalRecordCount) + _pageManager.TotalRecords = await _repository.CountAsync(entities); // pagination should be done last since it will execute the query var pagedEntities = await ApplyPageQueryAsync(entities); @@ -165,15 +135,14 @@ public virtual async Task GetAsync(TId id) } else { - entity = await _entities.GetAsync(id); + entity = await _repository.GetAsync(id); } - if(!IsNull(_hookExecutor, entity)) + 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} @@ -182,9 +151,8 @@ public virtual async Task GetAsync(TId id) // 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); + var entity = await _repository.GetAndIncludeAsync(id, relationshipName); if (!IsNull(_hookExecutor, entity)) { _hookExecutor.AfterRead(AsList(entity), ResourcePipeline.GetRelationship); @@ -201,11 +169,11 @@ public virtual async Task GetRelationshipAsync(TId id, string relationsh var resource = MapOut(entity); // compound-property -> CompoundProperty - var navigationPropertyName = _jsonApiContext.ResourceGraph.GetRelationshipName(relationshipName); + var navigationPropertyName = _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); + var relationshipValue = _resourceGraph.GetRelationship(resource, navigationPropertyName); return relationshipValue; } @@ -214,7 +182,7 @@ 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); + entity = await _repository.UpdateAsync(entity); if (!IsNull(_hookExecutor, entity)) { _hookExecutor.AfterUpdate(AsList(entity), ResourcePipeline.Patch); @@ -226,13 +194,13 @@ public virtual async Task UpdateAsync(TId id, TResource resource) // 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); + var entity = await _repository.GetAndIncludeAsync(id, relationshipName); if (entity == null) { throw new JsonApiException(404, $"Entity with id {id} could not be found."); } - var relationship = _jsonApiContext.ResourceGraph + var relationship = _resourceGraph .GetContextEntity(typeof(TResource)) .Relationships .FirstOrDefault(r => r.Is(relationshipName)); @@ -255,7 +223,7 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa 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); + await _repository.UpdateRelationshipsAsync(entity, relationship, relationshipIds); if (!IsNull(_hookExecutor, entity)) _hookExecutor.AfterUpdate(AsList(entity), ResourcePipeline.PatchRelationship); relationship.Type = relationshipType; @@ -263,70 +231,85 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa protected virtual async Task> ApplyPageQueryAsync(IQueryable entities) { - var pageManager = _jsonApiContext.PageManager; - if (!pageManager.IsPaginated) + if (!_pageManager.IsPaginated) { - var allEntities = await _entities.ToListAsync(entities); + var allEntities = await _repository.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"); + _logger?.LogInformation($"Applying paging query. Fetching page {_pageManager.CurrentPage} " + + $"with {_pageManager.PageSize} entities"); } - var pagedEntities = await _entities.PageAsync(entities, pageManager.PageSize, pageManager.CurrentPage); + var pagedEntities = await _repository.PageAsync(entities, _pageManager.PageSize, _pageManager.CurrentPage); return MapOut(pagedEntities); } protected virtual IQueryable ApplySortAndFilterQuery(IQueryable entities) { - var query = _jsonApiContext.QuerySet; + var query = _requestManager.QuerySet; - if (_jsonApiContext.QuerySet == null) + if (_requestManager.QuerySet == null) return entities; if (query.Filters.Count > 0) foreach (var filter in query.Filters) - entities = _entities.Filter(entities, filter); + entities = _repository.Filter(entities, filter); - entities = _entities.Sort(entities, query.SortParameters); + entities = _repository.Sort(entities, query.SortParameters); return entities; } + /// + /// Actually include the relationships + /// + /// + /// + /// protected virtual IQueryable IncludeRelationships(IQueryable entities, List relationships) { - _jsonApiContext.IncludedRelationships = relationships; foreach (var r in relationships) - entities = _entities.Include(entities, r); + { + entities = _repository.Include(entities, r); + } return entities; } + /// + /// Get the specified id with relationships + /// + /// + /// private async Task GetWithRelationshipsAsync(TId id) { - var query = _entities.Select(_entities.Get(), _jsonApiContext.QuerySet?.Fields).Where(e => e.Id.Equals(id)); + var query = _repository.Select(_repository.Get(), _requestManager.QuerySet?.Fields).Where(e => e.Id.Equals(id)); - _jsonApiContext.QuerySet.IncludedRelationships.ForEach(r => - { - query = _entities.Include(query, r); - }); + foreach (var r in _updatedFields.Relationships) + query = _repository.Include(query, r.InternalRelationshipName); TEntity value; // https://github.com/aspnet/EntityFrameworkCore/issues/6573 - if (_jsonApiContext.QuerySet?.Fields?.Count > 0) + if (_updatedFields.Attributes.Count() > 0) value = query.FirstOrDefault(); else - value = await _entities.FirstOrDefaultAsync(query); + value = await _repository.FirstOrDefaultAsync(query); + return value; } + private bool ShouldIncludeRelationships() + { + return _updatedFields.Relationships.Count() > 0; + } + private bool IsNull(params object[] values) { @@ -337,14 +320,15 @@ private bool IsNull(params object[] values) return false; } - private bool ShouldIncludeRelationships() - => (_jsonApiContext.QuerySet?.IncludedRelationships != null && - _jsonApiContext.QuerySet.IncludedRelationships.Count > 0); - + /// + /// Casts the entity given to `TResource` or maps it to its equal + /// + /// + /// private TResource MapOut(TEntity entity) - => (typeof(TResource) == typeof(TEntity)) - ? entity as TResource : - _mapper.Map(entity); + { + return (typeof(TResource) == typeof(TEntity)) ? entity as TResource : _mapper.Map(entity); + } private IEnumerable MapOut(IEnumerable entities) => (typeof(TResource) == typeof(TEntity)) @@ -361,4 +345,53 @@ private List AsList(TEntity entity) return new List { entity }; } } + /// + /// No mapping + /// + /// + /// + public class EntityResourceService : EntityResourceService, + IResourceService + where TResource : class, IIdentifiable + { + public EntityResourceService( + IEntityRepository repository, + IJsonApiOptions apiOptions, + IRequestContext requestManager, + IResourceGraph resourceGraph, + IPageQueryService pageManager, + ILoggerFactory loggerFactory = null, + IResourceHookExecutor hookExecutor = null) + : base(repository: repository, + options: apiOptions, + requestManager: requestManager, + pageManager: pageManager, + loggerFactory: loggerFactory, + resourceGraph: resourceGraph, + hookExecutor: hookExecutor) + { } + } + + /// + /// No mapping with integer as default + /// + /// + public class EntityResourceService : EntityResourceService, + IResourceService + where TResource : class, IIdentifiable + { + /// + /// Constructor for no mapping with integer as default + /// + public EntityResourceService( + IEntityRepository repository, + IJsonApiOptions options, + IRequestContext requestManager, + IPageQueryService pageManager, + IResourceGraph resourceGraph, + ILoggerFactory loggerFactory = null, + IResourceHookExecutor hookExecutor = null) : + base(repository: repository, apiOptions: options, requestManager, resourceGraph, pageManager, loggerFactory, hookExecutor) + { } + } } diff --git a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs index d3fb014bcc..3ccdcc0604 100644 --- a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs @@ -3,113 +3,32 @@ using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Request; namespace JsonApiDotNetCore.Services { public interface IJsonApiApplication { - JsonApiOptions Options { get; set; } + IJsonApiOptions Options { get; set; } + [Obsolete("Use standalone resourcegraph")] IResourceGraph ResourceGraph { get; set; } } public interface IQueryRequest { - List IncludedRelationships { get; set; } QuerySet QuerySet { get; set; } - PageManager PageManager { get; set; } } - public interface IUpdateRequest + public interface IJsonApiRequest : IJsonApiApplication, IQueryRequest { - /// - /// 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. + /// ` class. /// /// See [json-api/1254](https://github.com/json-api/json-api/pull/1254) for details. /// @@ -140,18 +59,4 @@ public interface IJsonApiRequest : IJsonApiApplication, IUpdateRequest, IQueryRe /// 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/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/OperationsProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/OperationsProcessor.cs index 275b4b85b0..e8ad318a2b 100644 --- a/src/JsonApiDotNetCore/Services/Operations/OperationsProcessor.cs +++ b/src/JsonApiDotNetCore/Services/Operations/OperationsProcessor.cs @@ -4,6 +4,8 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Data; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Models.Operations; using Microsoft.EntityFrameworkCore; @@ -20,15 +22,21 @@ public class OperationsProcessor : IOperationsProcessor private readonly IOperationProcessorResolver _processorResolver; private readonly DbContext _dbContext; private readonly IJsonApiContext _jsonApiContext; + private readonly IRequestContext _requestManager; + private readonly IResourceGraph _resourceGraph; public OperationsProcessor( IOperationProcessorResolver processorResolver, IDbContextResolver dbContextResolver, - IJsonApiContext jsonApiContext) + IJsonApiContext jsonApiContext, + IRequestContext requestManager, + IResourceGraph resourceGraph) { _processorResolver = processorResolver; _dbContext = dbContextResolver.GetContext(); _jsonApiContext = jsonApiContext; + _requestManager = requestManager; + _resourceGraph = resourceGraph; } public async Task> ProcessAsync(List inputOps) @@ -43,7 +51,8 @@ public async Task> ProcessAsync(List inputOps) { foreach (var op in inputOps) { - _jsonApiContext.BeginOperation(); + //_jsonApiContext.BeginOperation(); + lastAttemptedOperation = op.Op; await ProcessOperation(op, outputOps); opIndex++; @@ -70,6 +79,17 @@ private async Task ProcessOperation(Operation op, List outputOps) ReplaceLocalIdsInResourceObject(op.DataObject, outputOps); ReplaceLocalIdsInRef(op.Ref, outputOps); + string type = null; + if (op.Op == OperationCode.add || op.Op == OperationCode.update) + { + type = op.DataObject.Type; + } + else if (op.Op == OperationCode.get || op.Op == OperationCode.remove) + { + type = op.Ref.Type; + } + _requestManager.SetRequestResource(_resourceGraph.GetEntityFromControllerName(type)); + var processor = GetOperationsProcessor(op); var resultOp = await processor.ProcessAsync(op); @@ -96,7 +116,7 @@ private void ReplaceLocalIdsInResourceObject(ResourceObject resourceObject, List { foreach (var relationshipDictionary in resourceObject.Relationships) { - if (relationshipDictionary.Value.IsHasMany) + if (relationshipDictionary.Value.IsManyData) { foreach (var relationship in relationshipDictionary.Value.ManyData) if (HasLocalId(relationship)) diff --git a/src/JsonApiDotNetCore/Services/Operations/Processors/CreateOpProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/Processors/CreateOpProcessor.cs index ebc600a5fb..cc3a70faed 100644 --- a/src/JsonApiDotNetCore/Services/Operations/Processors/CreateOpProcessor.cs +++ b/src/JsonApiDotNetCore/Services/Operations/Processors/CreateOpProcessor.cs @@ -1,9 +1,13 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Models.Operations; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + +using JsonApiDotNetCore.Serialization.Contracts; namespace JsonApiDotNetCore.Services.Operations.Processors { @@ -21,10 +25,10 @@ public class CreateOpProcessor { public CreateOpProcessor( ICreateService service, - IJsonApiDeSerializer deSerializer, + IOperationsDeserializer deserializer, IDocumentBuilder documentBuilder, IResourceGraph resourceGraph - ) : base(service, deSerializer, documentBuilder, resourceGraph) + ) : base(service, deserializer, documentBuilder, resourceGraph) { } } @@ -32,25 +36,26 @@ public class CreateOpProcessor : ICreateOpProcessor where T : class, IIdentifiable { private readonly ICreateService _service; - private readonly IJsonApiDeSerializer _deSerializer; + private readonly IOperationsDeserializer _deserializer; private readonly IDocumentBuilder _documentBuilder; private readonly IResourceGraph _resourceGraph; public CreateOpProcessor( ICreateService service, - IJsonApiDeSerializer deSerializer, + IOperationsDeserializer deserializer, IDocumentBuilder documentBuilder, IResourceGraph resourceGraph) { _service = service; - _deSerializer = deSerializer; + _deserializer = deserializer; _documentBuilder = documentBuilder; _resourceGraph = resourceGraph; } public async Task ProcessAsync(Operation operation) { - var model = (T)_deSerializer.DocumentToObject(operation.DataObject); + + var model = (T)_deserializer.DocumentToObject(operation.DataObject); var result = await _service.CreateAsync(model); var operationResult = new Operation @@ -66,7 +71,8 @@ public async Task ProcessAsync(Operation operation) // can locate the result of this operation by its localId operationResult.DataObject.LocalId = operation.DataObject.LocalId; - return operationResult; + return null; + //return operationResult; } } } diff --git a/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs index 9415554bcb..e5b2d21918 100644 --- a/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs +++ b/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs @@ -4,9 +4,11 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Models.Operations; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; namespace JsonApiDotNetCore.Services.Operations.Processors { @@ -36,11 +38,11 @@ public GetOpProcessor( IGetAllService getAll, IGetByIdService getById, IGetRelationshipService getRelationship, - IJsonApiDeSerializer deSerializer, + IOperationsDeserializer deserializer, IDocumentBuilder documentBuilder, IResourceGraph resourceGraph, IJsonApiContext jsonApiContext - ) : base(getAll, getById, getRelationship, deSerializer, documentBuilder, resourceGraph, jsonApiContext) + ) : base(getAll, getById, getRelationship, deserializer, documentBuilder, resourceGraph, jsonApiContext) { } } @@ -51,7 +53,7 @@ public class GetOpProcessor : IGetOpProcessor private readonly IGetAllService _getAll; private readonly IGetByIdService _getById; private readonly IGetRelationshipService _getRelationship; - private readonly IJsonApiDeSerializer _deSerializer; + private readonly IOperationsDeserializer _deserializer; private readonly IDocumentBuilder _documentBuilder; private readonly IResourceGraph _resourceGraph; private readonly IJsonApiContext _jsonApiContext; @@ -61,7 +63,7 @@ public GetOpProcessor( IGetAllService getAll, IGetByIdService getById, IGetRelationshipService getRelationship, - IJsonApiDeSerializer deSerializer, + IOperationsDeserializer deserializer, IDocumentBuilder documentBuilder, IResourceGraph resourceGraph, IJsonApiContext jsonApiContext) @@ -69,7 +71,7 @@ public GetOpProcessor( _getAll = getAll; _getById = getById; _getRelationship = getRelationship; - _deSerializer = deSerializer; + _deserializer = deserializer; _documentBuilder = documentBuilder; _resourceGraph = resourceGraph; _jsonApiContext = jsonApiContext.ApplyContext(this); diff --git a/src/JsonApiDotNetCore/Services/Operations/Processors/RemoveOpProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/Processors/RemoveOpProcessor.cs index f3efe7939c..d84031c5e0 100644 --- a/src/JsonApiDotNetCore/Services/Operations/Processors/RemoveOpProcessor.cs +++ b/src/JsonApiDotNetCore/Services/Operations/Processors/RemoveOpProcessor.cs @@ -1,9 +1,13 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Models.Operations; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + +using JsonApiDotNetCore.Serialization.Contracts; namespace JsonApiDotNetCore.Services.Operations.Processors { @@ -20,10 +24,10 @@ public class RemoveOpProcessor : RemoveOpProcessor, IRemoveOpProcesso { public RemoveOpProcessor( IDeleteService service, - IJsonApiDeSerializer deSerializer, + IOperationsDeserializer deserializer, IDocumentBuilder documentBuilder, IResourceGraph resourceGraph - ) : base(service, deSerializer, documentBuilder, resourceGraph) + ) : base(service, deserializer, documentBuilder, resourceGraph) { } } @@ -31,18 +35,18 @@ public class RemoveOpProcessor : IRemoveOpProcessor where T : class, IIdentifiable { private readonly IDeleteService _service; - private readonly IJsonApiDeSerializer _deSerializer; + private readonly IOperationsDeserializer _deserializer; private readonly IDocumentBuilder _documentBuilder; private readonly IResourceGraph _resourceGraph; public RemoveOpProcessor( IDeleteService service, - IJsonApiDeSerializer deSerializer, + IOperationsDeserializer deserializer, IDocumentBuilder documentBuilder, IResourceGraph resourceGraph) { _service = service; - _deSerializer = deSerializer; + _deserializer = deserializer; _documentBuilder = documentBuilder; _resourceGraph = resourceGraph; } diff --git a/src/JsonApiDotNetCore/Services/Operations/Processors/UpdateOpProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/Processors/UpdateOpProcessor.cs index 9b136b30c0..ffe104000f 100644 --- a/src/JsonApiDotNetCore/Services/Operations/Processors/UpdateOpProcessor.cs +++ b/src/JsonApiDotNetCore/Services/Operations/Processors/UpdateOpProcessor.cs @@ -1,9 +1,13 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Models.Operations; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + +using JsonApiDotNetCore.Serialization.Contracts; namespace JsonApiDotNetCore.Services.Operations.Processors { @@ -20,10 +24,10 @@ public class UpdateOpProcessor : UpdateOpProcessor, IUpdateOpProcesso { public UpdateOpProcessor( IUpdateService service, - IJsonApiDeSerializer deSerializer, + IOperationsDeserializer deserializer, IDocumentBuilder documentBuilder, IResourceGraph resourceGraph - ) : base(service, deSerializer, documentBuilder, resourceGraph) + ) : base(service, deserializer, documentBuilder, resourceGraph) { } } @@ -31,18 +35,18 @@ public class UpdateOpProcessor : IUpdateOpProcessor where T : class, IIdentifiable { private readonly IUpdateService _service; - private readonly IJsonApiDeSerializer _deSerializer; + private readonly IOperationsDeserializer _deserializer; private readonly IDocumentBuilder _documentBuilder; private readonly IResourceGraph _resourceGraph; public UpdateOpProcessor( IUpdateService service, - IJsonApiDeSerializer deSerializer, + IOperationsDeserializer deserializer, IDocumentBuilder documentBuilder, IResourceGraph resourceGraph) { _service = service; - _deSerializer = deSerializer; + _deserializer = deserializer; _documentBuilder = documentBuilder; _resourceGraph = resourceGraph; } @@ -52,7 +56,8 @@ 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 model = (T)_deserializer.DocumentToObject(operation.DataObject); + T model = null; // TODO var result = await _service.UpdateAsync(model.Id, model); if (result == null) diff --git a/src/JsonApiDotNetCore/Services/QueryAccessor.cs b/src/JsonApiDotNetCore/Services/QueryAccessor.cs index dc9ff7ef0a..4b8204d8a4 100644 --- a/src/JsonApiDotNetCore/Services/QueryAccessor.cs +++ b/src/JsonApiDotNetCore/Services/QueryAccessor.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Managers.Contracts; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Services @@ -17,19 +18,28 @@ public interface IQueryAccessor T GetRequired(string key); } + /// + /// Accessing queries + /// public class QueryAccessor : IQueryAccessor { - private readonly IJsonApiContext _jsonApiContext; + private readonly IRequestContext _requestManager; private readonly ILogger _logger; + /// + /// Creates an instance which can be used to access the qury + /// + /// + /// public QueryAccessor( - IJsonApiContext jsonApiContext, + IRequestContext requestManager, ILogger logger) { - _jsonApiContext = jsonApiContext; + _requestManager = requestManager; _logger = logger; } + public T GetRequired(string key) { if (TryGetValue(key, out T result) == false) @@ -71,13 +81,13 @@ public bool TryGetValue(string key, out T value) } private string GetFilterValue(string key) { - var publicValue = _jsonApiContext.QuerySet.Filters + var publicValue = _requestManager.QuerySet.Filters .FirstOrDefault(f => string.Equals(f.Attribute, key, StringComparison.OrdinalIgnoreCase))?.Value; if(publicValue != null) return publicValue; - var internalValue = _jsonApiContext.QuerySet.Filters + var internalValue = _requestManager.QuerySet.Filters .FirstOrDefault(f => string.Equals(f.Attribute, key, StringComparison.OrdinalIgnoreCase))?.Value; if(internalValue != null) { diff --git a/src/JsonApiDotNetCore/Services/QueryComposer.cs b/src/JsonApiDotNetCore/Services/QueryComposer.cs index e365811704..9734c5e9a9 100644 --- a/src/JsonApiDotNetCore/Services/QueryComposer.cs +++ b/src/JsonApiDotNetCore/Services/QueryComposer.cs @@ -1,21 +1,22 @@ using System.Collections.Generic; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; namespace JsonApiDotNetCore.Services { public interface IQueryComposer { - string Compose(IJsonApiContext jsonApiContext); + string Compose(IRequestContext jsonApiContext); } public class QueryComposer : IQueryComposer { - public string Compose(IJsonApiContext jsonApiContext) + public string Compose(IRequestContext requestManager) { string result = ""; - if (jsonApiContext != null && jsonApiContext.QuerySet != null) + if (requestManager != null && requestManager.QuerySet != null) { - List filterQueries = jsonApiContext.QuerySet.Filters; + List filterQueries = requestManager.QuerySet.Filters; if (filterQueries.Count > 0) { foreach (FilterQuery filter in filterQueries) diff --git a/src/JsonApiDotNetCore/Services/QueryParser.cs b/src/JsonApiDotNetCore/Services/QueryParser.cs index bffcdc9a59..da1d193393 100644 --- a/src/JsonApiDotNetCore/Services/QueryParser.cs +++ b/src/JsonApiDotNetCore/Services/QueryParser.cs @@ -4,12 +4,15 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using Microsoft.AspNetCore.Http; namespace JsonApiDotNetCore.Services { + public interface IQueryParser { QuerySet Parse(IQueryCollection query); @@ -17,53 +20,65 @@ public interface IQueryParser public class QueryParser : IQueryParser { - private readonly IControllerContext _controllerContext; - private readonly JsonApiOptions _options; - - public QueryParser( - IControllerContext controllerContext, - JsonApiOptions options) + private readonly IInternalIncludedQueryService _includedQuery; + private readonly IInternalFieldsQueryService _fieldQuery; + private readonly IPageQueryService _pageQuery; + private readonly IRequestContext _requestManager; + private readonly IJsonApiOptions _options; + private readonly ContextEntity _requestResource; + private readonly IContextEntityProvider _provider; + + public QueryParser(IInternalIncludedQueryService includedRelationships, + IInternalFieldsQueryService fieldQuery, + IRequestContext requestManager, + IPageQueryService pageQuery, + IContextEntityProvider provider, + IJsonApiOptions options) { - _controllerContext = controllerContext; + _includedQuery = includedRelationships; + _fieldQuery = fieldQuery; + _requestManager = requestManager; + _pageQuery = pageQuery; + _provider = provider; + _requestResource = requestManager.GetRequestResource(); _options = options; } public virtual QuerySet Parse(IQueryCollection query) { var querySet = new QuerySet(); - var disabledQueries = _controllerContext.GetControllerAttribute()?.QueryParams ?? QueryParams.None; - + var disabledQueries = _requestManager.DisabledQueryParams; foreach (var pair in query) { - if (pair.Key.StartsWith(QueryConstants.FILTER)) + if (pair.Key.StartsWith(QueryConstants.FILTER, StringComparison.Ordinal)) { - if (disabledQueries.HasFlag(QueryParams.Filter) == false) + if (disabledQueries.HasFlag(QueryParams.Filters) == false) querySet.Filters.AddRange(ParseFilterQuery(pair.Key, pair.Value)); continue; } - if (pair.Key.StartsWith(QueryConstants.SORT)) + if (pair.Key.StartsWith(QueryConstants.SORT, StringComparison.Ordinal)) { if (disabledQueries.HasFlag(QueryParams.Sort) == false) querySet.SortParameters = ParseSortParameters(pair.Value); continue; } - if (pair.Key.StartsWith(QueryConstants.INCLUDE)) + if (pair.Key.StartsWith(QueryConstants.INCLUDE, StringComparison.Ordinal)) { if (disabledQueries.HasFlag(QueryParams.Include) == false) querySet.IncludedRelationships = ParseIncludedRelationships(pair.Value); continue; } - if (pair.Key.StartsWith(QueryConstants.PAGE)) + if (pair.Key.StartsWith(QueryConstants.PAGE, StringComparison.Ordinal)) { if (disabledQueries.HasFlag(QueryParams.Page) == false) querySet.PageQuery = ParsePageQuery(querySet.PageQuery, pair.Key, pair.Value); continue; } - if (pair.Key.StartsWith(QueryConstants.FIELDS)) + if (pair.Key.StartsWith(QueryConstants.FIELDS, StringComparison.Ordinal)) { if (disabledQueries.HasFlag(QueryParams.Fields) == false) querySet.Fields = ParseFieldsQuery(pair.Key, pair.Value); @@ -133,9 +148,11 @@ protected virtual PageQuery ParsePageQuery(PageQuery pageQuery, string key, stri 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) ? @@ -153,7 +170,7 @@ protected virtual List ParseSortParameters(string value) const char DESCENDING_SORT_OPERATOR = '-'; var sortSegments = value.Split(QueryConstants.COMMA); - if(sortSegments.Where(s => s == string.Empty).Count() >0) + if (sortSegments.Where(s => s == string.Empty).Count() > 0) { throw new JsonApiException(400, "The sort URI segment contained a null value."); } @@ -176,9 +193,29 @@ protected virtual List ParseSortParameters(string value) protected virtual List ParseIncludedRelationships(string value) { - return value - .Split(QueryConstants.COMMA) - .ToList(); + var inclusions = value.Split(QueryConstants.COMMA).ToList(); + foreach (var chain in inclusions) + { + var parsedChain = new List(); + var resourceContext = _requestManager.GetRequestResource(); + var splittedPath = chain.Split(QueryConstants.DOT); + foreach (var requestedRelationship in splittedPath) + { + var relationship = resourceContext.Relationships.Single(r => r.PublicRelationshipName == requestedRelationship); + if (relationship == null) + throw new JsonApiException(400, $"Invalid relationship {requestedRelationship} on {resourceContext.EntityName}", + $"{resourceContext.EntityName} does not have a relationship named {requestedRelationship}"); + + if (relationship.CanInclude == false) + throw new JsonApiException(400, $"Including the relationship {requestedRelationship} on {resourceContext.EntityName} is not allowed"); + + parsedChain.Add(relationship); + resourceContext = _provider.GetContextEntity(relationship.PrincipalType); + } + _includedQuery.Register(parsedChain); + } + + return inclusions; } protected virtual List ParseFieldsQuery(string key, string value) @@ -187,8 +224,8 @@ protected virtual List ParseFieldsQuery(string key, string value) 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) + var relationship = _requestResource.Relationships.SingleOrDefault(a => a.Is(typeName)); + if (relationship == default && string.Equals(typeName, _requestResource.EntityName, StringComparison.OrdinalIgnoreCase) == false) return includedFields; var fields = value.Split(QueryConstants.COMMA); @@ -196,20 +233,23 @@ protected virtual List ParseFieldsQuery(string key, string value) { if (relationship != default) { - var relationProperty = _options.ResourceGraph.GetContextEntity(relationship.DependentType); + var relationProperty = _resourceGraph.GetContextEntity(relationship.DependentType); var attr = relationProperty.Attributes.SingleOrDefault(a => a.Is(field)); - if(attr == null) + if (attr == null) throw new JsonApiException(400, $"'{relationship.DependentType.Name}' does not contain '{field}'."); + _fieldQuery.Register(attr, relationship); // e.g. "Owner.Name" includedFields.Add(relationship.InternalRelationshipName + "." + attr.InternalAttributeName); + } else { - var attr = _controllerContext.RequestEntity.Attributes.SingleOrDefault(a => a.Is(field)); + var attr = _requestResource.Attributes.SingleOrDefault(a => a.Is(field)); if (attr == null) - throw new JsonApiException(400, $"'{_controllerContext.RequestEntity.EntityName}' does not contain '{field}'."); + throw new JsonApiException(400, $"'{_requestResource.EntityName}' does not contain '{field}'."); + _fieldQuery.Register(attr, relationship); // e.g. "Name" includedFields.Add(attr.InternalAttributeName); } @@ -222,14 +262,13 @@ protected virtual AttrAttribute GetAttribute(string propertyName) { try { - return _controllerContext - .RequestEntity + return _requestResource .Attributes .Single(attr => attr.Is(propertyName)); } catch (InvalidOperationException e) { - throw new JsonApiException(400, $"Attribute '{propertyName}' does not exist on resource '{_controllerContext.RequestEntity.EntityName}'", e); + throw new JsonApiException(400, $"Attribute '{propertyName}' does not exist on resource '{_requestResource.EntityName}'", e); } } diff --git a/src/JsonApiDotNetCore/Services/ResourceFieldExplorer.cs b/src/JsonApiDotNetCore/Services/ResourceFieldExplorer.cs new file mode 100644 index 0000000000..869d3067d7 --- /dev/null +++ b/src/JsonApiDotNetCore/Services/ResourceFieldExplorer.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Services +{ + + public class ExposedFieldExplorer : IExposedFieldExplorer + { + private readonly IContextEntityProvider _provider; + + public ExposedFieldExplorer(IContextEntityProvider provider) + { + _provider = provider; + } + + public List GetFields(Expression> selector = null) where T : IIdentifiable + { + return Getter(selector).ToList(); + } + + public List GetAttributes(Expression> selector = null) where T : IIdentifiable + { + return Getter(selector, FieldFilterType.Attribute).Cast().ToList(); + } + + public List GetRelationships(Expression> selector = null) where T : IIdentifiable + { + return Getter(selector, FieldFilterType.Relationship).Cast().ToList(); + } + + public List GetFields(Type type) + { + return _provider.GetContextEntity(type).Fields.ToList(); + } + + public List GetAttributes(Type type) + { + return _provider.GetContextEntity(type).Attributes.ToList(); + } + + public List GetRelationships(Type type) + { + return _provider.GetContextEntity(type).Relationships.ToList(); + } + + private IEnumerable Getter(Expression> selector = null, FieldFilterType type = FieldFilterType.None) where T : IIdentifiable + { + IEnumerable available; + if (type == FieldFilterType.Attribute) + available = _provider.GetContextEntity(typeof(T)).Attributes.Cast(); + else if (type == FieldFilterType.Relationship) + available = _provider.GetContextEntity(typeof(T)).Relationships.Cast(); + else + available = _provider.GetContextEntity(typeof(T)).Fields; + + if (selector == null) + return available; + + var targeted = new List(); + // model => model.Field1 + if (selector.Body is MemberExpression memberExpression) + { + try + { + targeted.Add(available.Single(f => f.ExposedInternalMemberName == memberExpression.Member.Name)); + return targeted; + } + catch (Exception ex) + { + ThrowNotExposedError(memberExpression.Member.Name, type); + } + } + + // model => new { model.Field1, model.Field2 } + if (selector.Body is NewExpression newExpression) + { + 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 (Exception ex) + { + 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 };"); + + } + + private void ThrowNotExposedError(string memberName, FieldFilterType type) + { + throw new ArgumentException($"{memberName} is not an json:api exposed {type.ToString("g")}."); + } + private enum FieldFilterType + { + None, + Attribute, + Relationship + } + } + + public interface IExposedFieldExplorer + { + List GetFields(Expression> selector = null) where T : IIdentifiable; + List GetAttributes(Expression> selector = null) where T : IIdentifiable; + List GetRelationships(Expression> selector = null) where T : IIdentifiable; + List GetFields(Type type); + List GetAttributes(Type type); + List GetRelationships(Type type); + } +} diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 09ffcfcfc1..5f433e4262 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -1,12 +1,18 @@ using GettingStarted.Models; using GettingStarted.ResourceDefinitionExample; using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Data; using JsonApiDotNetCore.Graph; +using JsonApiDotNetCore.Hooks; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Moq; using Xunit; @@ -58,13 +64,20 @@ public void AddCurrentAssembly_Adds_Resources_To_Graph() [Fact] public void AddCurrentAssembly_Adds_Services_To_Container() - { - // arrange, act + { + // arrange, act + _services.AddSingleton(new JsonApiOptions()); + + _services.AddScoped((_) => new Mock().Object); + _services.AddScoped((_) => new Mock().Object); + _services.AddScoped((_) => new Mock().Object); + _services.AddScoped((_) => new Mock().Object); _facade.AddCurrentAssembly(); // assert var services = _services.BuildServiceProvider(); - Assert.IsType(services.GetService>()); + var service = services.GetService>(); + Assert.IsType(service); } [Fact] @@ -83,14 +96,24 @@ 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) { } + private static IJsonApiContext _jsonApiContext = new Mock().Object; + + public TestModelService( + IEntityRepository repository, + IJsonApiOptions options, + IRequestContext requestManager, + IPageQueryService pageManager, + IResourceGraph resourceGraph, + ILoggerFactory loggerFactory = null, + IResourceHookExecutor hookExecutor = null) : base(repository, options, requestManager, pageManager, resourceGraph, loggerFactory, hookExecutor) + { + } } public class TestModelRepository : DefaultEntityRepository { internal static IDbContextResolver _dbContextResolver; - private static IJsonApiContext _jsonApiContext = new Mock().Object; + private static IJsonApiContext _jsonApiContext = new Mock().Object; public TestModelRepository() : base(_jsonApiContext, _dbContextResolver) { } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/CamelCasedModelsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/CamelCasedModelsControllerTests.cs index b2059c10c9..07c9be984e 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/CamelCasedModelsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/CamelCasedModelsControllerTests.cs @@ -5,6 +5,12 @@ using System.Threading.Tasks; using Bogus; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + +using JsonApiDotNetCore.Serialization.Contracts; + +using JsonApiDotNetCore.Serialization.Contracts; + using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; @@ -52,8 +58,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.GetService().DeserializeList(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -80,7 +85,7 @@ public async Task Can_Get_CamelCasedModels_ById() // Act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (CamelCasedModel)_fixture.GetService() + var deserializedBody = (CamelCasedModel)_fixture.GetService() .Deserialize(body); // Assert @@ -124,7 +129,7 @@ public async Task Can_Post_CamelCasedModels() Assert.NotNull(body); Assert.NotEmpty(body); - var deserializedBody = (CamelCasedModel)_fixture.GetService() + var deserializedBody = (CamelCasedModel)_fixture.GetService() .Deserialize(body); Assert.Equal(model.CompoundAttr, deserializedBody.CompoundAttr); } @@ -152,8 +157,7 @@ public async Task Can_Patch_CamelCasedModels() }; var httpMethod = new HttpMethod("PATCH"); var route = $"/camelCasedModels/{model.Id}"; - var builder = new WebHostBuilder() - .UseStartup(); + var builder = new WebHostBuilder().UseStartup(); var server = new TestServer(builder); var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); @@ -169,7 +173,7 @@ public async Task Can_Patch_CamelCasedModels() Assert.NotNull(body); Assert.NotEmpty(body); - var deserializedBody = (CamelCasedModel)_fixture.GetService() + var deserializedBody = (CamelCasedModel)_fixture.GetService() .Deserialize(body); Assert.Equal(newModel.CompoundAttr, deserializedBody.CompoundAttr); } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs index 478f40f14f..3b9fd699c7 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs @@ -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,8 +118,10 @@ 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(); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs index fcc6e5ffde..651d096225 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs @@ -1,6 +1,8 @@ using Newtonsoft.Json; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + using Xunit; namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility @@ -27,7 +29,7 @@ public void Can_Return_Custom_Error_Types() }); // act - var result = new JsonApiSerializer(null, null, null) + var result = new JsonApiSerializer(null, null, null,null) .Serialize(errorCollection); // assert diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs index 2030694918..40c74b4500 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs @@ -82,7 +82,7 @@ public async Task CheckNullBehaviorCombination(bool? omitNullValuedAttributes, b { nullAttributeResponseBehavior = new NullAttributeResponseBehavior(); } - var jsonApiOptions = _fixture.GetService(); + var jsonApiOptions = _fixture.GetService(); jsonApiOptions.NullAttributeResponseBehavior = nullAttributeResponseBehavior; jsonApiOptions.AllowCustomQueryParameters = true; 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..d69280e7e7 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs @@ -40,7 +40,8 @@ public async Task Injecting_IRequestMeta_Adds_Meta_Data() // act var response = await client.SendAsync(request); - var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + var body = await response.Content.ReadAsStringAsync(); + var documents = JsonConvert.DeserializeObject(body); // assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs index 90496b3690..88906ba7ab 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,14 +14,14 @@ 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); } @@ -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..ab60a03c3b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs @@ -8,6 +8,8 @@ using Bogus; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Microsoft.EntityFrameworkCore; @@ -62,7 +64,7 @@ public async Task Can_Fetch_Many_To_Many_Through_All() var document = JsonConvert.DeserializeObject(body); Assert.NotEmpty(document.Included); - var articleResponseList = _fixture.GetService().DeserializeList
(body); + var articleResponseList = _fixture.GetService().DeserializeList
(body); Assert.NotNull(articleResponseList); var articleResponse = articleResponseList.FirstOrDefault(a => a.Id == article.Id); @@ -101,7 +103,7 @@ public async Task Can_Fetch_Many_To_Many_Through_GetById() var document = JsonConvert.DeserializeObject(body); Assert.NotEmpty(document.Included); - var articleResponse = _fixture.GetService().Deserialize
(body); + var articleResponse = _fixture.GetService().Deserialize
(body); Assert.NotNull(articleResponse); Assert.Equal(article.Id, articleResponse.Id); @@ -190,7 +192,7 @@ public async Task Can_Create_Many_To_Many() 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.GetService().Deserialize
(body); Assert.NotNull(articleResponse); var persistedArticle = await _fixture.Context.Articles @@ -243,7 +245,7 @@ public async Task Can_Update_Many_To_Many() 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.GetService().Deserialize
(body); Assert.NotNull(articleResponse); _fixture.ReloadDbContext(); @@ -303,7 +305,7 @@ public async Task Can_Update_Many_To_Many_With_Complete_Replacement() 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.GetService().Deserialize
(body); Assert.NotNull(articleResponse); _fixture.ReloadDbContext(); @@ -366,7 +368,7 @@ public async Task Can_Update_Many_To_Many_With_Complete_Replacement_With_Overlap 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.GetService().Deserialize
(body); Assert.NotNull(articleResponse); _fixture.ReloadDbContext(); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/QueryFiltersTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/QueryFiltersTests.cs index 92589dd0b5..3c09e4061b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/QueryFiltersTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/QueryFiltersTests.cs @@ -7,6 +7,8 @@ using Bogus; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Microsoft.EntityFrameworkCore; @@ -50,7 +52,7 @@ public async Task FiltersWithCustomQueryFiltersEquals() // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService().DeserializeList(body); + 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)); } @@ -78,7 +80,7 @@ public async Task FiltersWithCustomQueryFiltersLessThan() // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService().DeserializeList(body); + var deserializedBody = _fixture.GetService().DeserializeList(body); 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..f56b4edc84 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs @@ -8,6 +8,8 @@ using Bogus; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Microsoft.EntityFrameworkCore; @@ -101,7 +103,7 @@ public async Task Can_Create_User_With_Password() // response assertions var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (User)_fixture.GetService().Deserialize(body); + var deserializedBody = (User)_fixture.GetService().Deserialize(body); var document = JsonConvert.DeserializeObject(body); Assert.False(document.Data.Attributes.ContainsKey("password")); Assert.Equal(user.Username, document.Data.Attributes["username"]); @@ -150,7 +152,7 @@ public async Task Can_Update_User_Password() // response assertions var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (User)_fixture.GetService().Deserialize(body); + var deserializedBody = (User)_fixture.GetService().Deserialize(body); var document = JsonConvert.DeserializeObject(body); Assert.False(document.Data.Attributes.ContainsKey("password")); Assert.Equal(user.Username, document.Data.Attributes["username"]); @@ -206,10 +208,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..48eec4adb5 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs @@ -7,6 +7,8 @@ using Bogus; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Newtonsoft.Json; @@ -52,7 +54,7 @@ public async Task Can_Filter_On_Guid_Properties() var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); var deserializedBody = _fixture - .GetService() + .GetService() .DeserializeList(body); var todoItemResponse = deserializedBody.Single(); @@ -125,7 +127,7 @@ public async Task Can_Filter_On_Not_Equal_Values() var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); var deserializedTodoItems = _fixture - .GetService() + .GetService() .DeserializeList(body); // assert @@ -161,7 +163,7 @@ public async Task Can_Filter_On_In_Array_Values() var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); var deserializedTodoItems = _fixture - .GetService() + .GetService() .DeserializeList(body); // assert @@ -215,6 +217,8 @@ public async Task Can_Filter_On_Not_In_Array_Values() { // 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(); @@ -238,7 +242,7 @@ public async Task Can_Filter_On_Not_In_Array_Values() var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); var deserializedTodoItems = _fixture - .GetService() + .GetService() .DeserializeList(body); // assert diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs index 93137b4ee2..b76f6ca3e5 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -7,6 +7,8 @@ using System.Threading.Tasks; using Bogus; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; @@ -84,6 +86,7 @@ public async Task Can_Create_Guid_Identifiable_Entity() // act var response = await client.SendAsync(request); + var sdfsd = await response.Content.ReadAsStringAsync(); // assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); @@ -163,7 +166,7 @@ public async Task Can_Create_Entity_With_Client_Defined_Id_If_Configured() // act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); + var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); // assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); @@ -215,7 +218,7 @@ public async Task Can_Create_Guid_Identifiable_Entity_With_Client_Defined_Id_If_ // act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItemCollection)_fixture.GetService().Deserialize(body); + var deserializedBody = (TodoItemCollection)_fixture.GetService().Deserialize(body); // assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); @@ -234,9 +237,11 @@ public async Task Can_Create_And_Set_HasMany_Relationships() var context = _fixture.GetService(); - var owner = new JsonApiDotNetCoreExample.Models.Person(); - var todoItem = new TodoItem(); - todoItem.Owner = owner; + var owner = new Person(); + var todoItem = new TodoItem + { + Owner = owner + }; context.People.Add(owner); context.TodoItems.Add(todoItem); await context.SaveChangesAsync(); @@ -276,7 +281,7 @@ public async Task Can_Create_And_Set_HasMany_Relationships() // act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItemCollection)_fixture.GetService().Deserialize(body); + var deserializedBody = (TodoItemCollection)_fixture.GetService().Deserialize(body); var newId = deserializedBody.Id; context = _fixture.GetService(); @@ -349,7 +354,7 @@ public async Task Can_Create_With_HasMany_Relationship_And_Include_Result() // assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var collectionResult = _fixture.GetService().Deserialize(body); + var collectionResult = _fixture.GetService().Deserialize(body); Assert.NotNull(collectionResult); Assert.NotEmpty(collectionResult.TodoItems); @@ -402,7 +407,7 @@ public async Task Can_Create_And_Set_HasOne_Relationships() // assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); - var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); + var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); var newId = deserializedBody.Id; context = _fixture.GetService(); @@ -463,7 +468,7 @@ public async Task Can_Create_With_HasOne_Relationship_And_Include_Result() // assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); - var todoItemResult = (TodoItem)_fixture.GetService().Deserialize(body); + var todoItemResult = (TodoItem)_fixture.GetService().Deserialize(body); Assert.NotNull(todoItemResult); Assert.NotNull(todoItemResult.Owner); Assert.Equal(owner.FirstName, todoItemResult.Owner.FirstName); @@ -516,7 +521,7 @@ public async Task Can_Create_And_Set_HasOne_Relationships_From_Independent_Side( // assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); - var deserializedBody = (PersonRole)_fixture.GetService().Deserialize(body); + var deserializedBody = (PersonRole)_fixture.GetService().Deserialize(body); Assert.Equal(person.Id, deserializedBody.Person.Id); } @@ -551,7 +556,7 @@ public async Task ShouldReceiveLocationHeader_InResponse() // act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); + var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); // assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); @@ -632,7 +637,7 @@ public async Task Create_With_ToOne_Relationship_With_Implicit_Remove() // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var personResult = _fixture.GetService().Deserialize(body); + var personResult = _fixture.GetService().Deserialize(body); // Assert @@ -691,7 +696,7 @@ public async Task Create_With_ToMany_Relationship_With_Implicit_Remove() // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var personResult = _fixture.GetService().Deserialize(body); + var personResult = _fixture.GetService().Deserialize(body); // Assert Assert.True(HttpStatusCode.Created == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}"); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs index 5e4754c7c5..5421d72391 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs @@ -57,7 +57,7 @@ public async Task Can_Include_Nested_Relationships() Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var todoItems = _fixture.DeSerializer.DeserializeList(body); + var todoItems = _fixture.deserializer.DeserializeList(body); var responseTodoItem = Assert.Single(todoItems); Assert.NotNull(responseTodoItem); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs index e277c8d0af..cf61c10805 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs @@ -43,7 +43,7 @@ public Included(TestFixture fixture) } [Fact] - public async Task GET_Included_Contains_SideloadedData_ForManyToOne() + public async Task GET_Included_Contains_SideloadeData_ForManyToOne() { // arrange var person = _personFaker.Generate(); @@ -77,7 +77,7 @@ public async Task GET_Included_Contains_SideloadedData_ForManyToOne() } [Fact] - public async Task GET_ById_Included_Contains_SideloadedData_ForManyToOne() + public async Task GET_ById_Included_Contains_SideloadeData_ForManyToOne() { // arrange var person = _personFaker.Generate(); @@ -111,7 +111,7 @@ public async Task GET_ById_Included_Contains_SideloadedData_ForManyToOne() } [Fact] - public async Task GET_Included_Contains_SideloadedData_OneToMany() + public async Task GET_Included_Contains_SideloadeData_OneToMany() { // arrange _context.People.RemoveRange(_context.People); // ensure all people have todo-items @@ -214,7 +214,7 @@ public async Task GET_Included_DoesNot_Duplicate_Records_If_HasOne_Exists_Twice( } [Fact] - public async Task GET_ById_Included_Contains_SideloadedData_ForOneToMany() + public async Task GET_ById_Included_Contains_SideloadeData_ForOneToMany() { // arrange const int numberOfTodoItems = 5; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs index 9c8d5f8214..db9775ba4d 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs @@ -5,6 +5,8 @@ using Bogus; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; @@ -62,7 +64,7 @@ public async Task Request_ForEmptyCollection_Returns_EmptyDataCollection() // act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService().DeserializeList(body); + var deserializedBody = _fixture.GetService().DeserializeList(body); // assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PagingTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PagingTests.cs index 0667b51756..67be776f1a 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PagingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PagingTests.cs @@ -5,6 +5,8 @@ using Bogus; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + using JsonApiDotNetCoreExample.Models; using Xunit; using Person = JsonApiDotNetCoreExample.Models.Person; @@ -42,7 +44,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 = GetService().DeserializeList(body); Assert.NotEmpty(deserializedBody); Assert.Equal(expectedEntitiesPerPage, deserializedBody.Count); @@ -73,7 +75,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 = GetService().DeserializeList(body); var expectedTodoItems = new[] { todoItems[0], todoItems[1] }; Assert.Equal(expectedTodoItems, deserializedBody, new IdComparer()); @@ -104,7 +106,7 @@ public async Task Can_Paginate_TodoItems_From_End() Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = GetService().DeserializeList(body); + var deserializedBody = GetService().DeserializeList(body); var expectedTodoItems = new[] { todoItems[totalCount - 2], todoItems[totalCount - 1] }; Assert.Equal(expectedTodoItems, deserializedBody, new IdComparer()); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/QueryParameters.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/QueryParameters.cs index 58b2323da5..d530f70d40 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/QueryParameters.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/QueryParameters.cs @@ -35,12 +35,13 @@ public async Task Server_Returns_400_ForUnknownQueryParam() // 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.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..69018e8fb5 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs @@ -19,6 +19,8 @@ using Person = JsonApiDotNetCoreExample.Models.Person; using System.Net; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec { @@ -146,7 +148,7 @@ public async Task Fields_Query_Selects_All_Fieldset_With_HasOne() Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); var deserializedTodoItems = _fixture - .GetService() + .GetService() .DeserializeList(body); foreach(var item in deserializedTodoItems.Where(i => i.Owner != null)) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs index 6d6de345a0..a1555f3fe0 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs @@ -1,6 +1,8 @@ using System; using System.Net.Http; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + using JsonApiDotNetCoreExample.Data; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; @@ -25,13 +27,13 @@ public TestFixture() Client = _server.CreateClient(); Context = GetService().GetContext() as AppDbContext; - DeSerializer = GetService(); + deserializer = GetService(); JsonApiContext = GetService(); } public HttpClient Client { get; set; } public AppDbContext Context { get; private set; } - public IJsonApiDeSerializer DeSerializer { get; private set; } + public IJsonApiDeserializer deserializer { get; private set; } public IJsonApiContext JsonApiContext { get; private set; } public T GetService() => (T)_services.GetService(typeof(T)); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs index 7ce1604683..05268e2cb0 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs @@ -8,6 +8,8 @@ using Bogus; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -44,15 +46,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 = _jsonApiContext.Options.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 +70,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.GetService().DeserializeList(body); // 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 +93,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.GetService().DeserializeList(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -96,7 +105,7 @@ public async Task Can_Filter_By_Resource_Id() public async Task Can_Filter_By_Relationship_Id() { // Arrange - var person = new Person(); + var person = new Person(); var todoItem = _todoItemFaker.Generate(); todoItem.Owner = person; _context.TodoItems.Add(todoItem); @@ -109,7 +118,7 @@ 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.GetService().DeserializeList(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -135,7 +144,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.GetService().DeserializeList(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -168,7 +177,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.GetService().DeserializeList(body); // Assert Assert.NotEmpty(todoItems); @@ -198,7 +207,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.GetService().DeserializeList(body); // Assert Assert.NotEmpty(todoItems); @@ -222,7 +231,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.GetService().DeserializeList(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -257,7 +266,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.GetService().DeserializeList(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -298,7 +307,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.GetService().DeserializeList(body); Assert.NotEmpty(deserializedBody); long lastAge = 0; @@ -336,7 +345,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.GetService().DeserializeList(body); Assert.NotEmpty(deserializedBody); int maxAge = deserializedBody.Max(i => i.Owner.Age) + 1; @@ -372,7 +381,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.GetService().DeserializeList(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -403,7 +412,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 = (TodoItem)_fixture.GetService().Deserialize(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -434,7 +443,7 @@ public async Task Can_Get_TodoItem_WithOwner() // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); + var deserializedBody = (TodoItem)_fixture.GetService().Deserialize(body); Assert.Equal(person.Id, deserializedBody.Owner.Id); Assert.Equal(todoItem.Id, deserializedBody.Id); @@ -493,7 +502,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 = (TodoItem)_fixture.GetService().Deserialize(body); Assert.Equal(HttpStatusCode.Created, response.StatusCode); Assert.Equal(todoItem.Description, deserializedBody.Description); Assert.Equal(todoItem.CreatedDate.ToString("G"), deserializedBody.CreatedDate.ToString("G")); @@ -609,7 +618,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 = (TodoItem)_fixture.GetService().Deserialize(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -662,7 +671,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 = (TodoItem)_fixture.GetService().Deserialize(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -714,7 +723,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 = (TodoItem)_fixture.GetService().Deserialize(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); 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 index 32d8186802..8048738417 100644 --- a/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/ClientGeneratedIdsStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/ClientGeneratedIdsStartup.cs @@ -6,6 +6,7 @@ using JsonApiDotNetCore.Extensions; using System; using JsonApiDotNetCoreExample; +using System.Reflection; namespace JsonApiDotNetCoreExampleTests.Startups { @@ -18,25 +19,24 @@ public ClientGeneratedIdsStartup(IHostingEnvironment 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; - }); + loggerFactory.AddConsole(LogLevel.Warning); + var mvcBuilder = services.AddMvcCore(); + services + .AddSingleton(loggerFactory) + .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); return services.BuildServiceProvider(); + } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/MetaStartup.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/MetaStartup.cs index 6bc5a08016..1aa7614a7e 100644 --- a/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/MetaStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/Helpers/Startups/MetaStartup.cs @@ -1,9 +1,5 @@ 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; @@ -19,21 +15,8 @@ public MetaStartup(IHostingEnvironment 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(); + services.AddScoped(); + return base.ConfigureServices(services); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj b/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj index b4fcaf7ae0..91471ee7c0 100644 --- a/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj +++ b/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj @@ -24,9 +24,17 @@ + + + + + + + + diff --git a/test/NoEntityFrameworkTests/Acceptance/Extensibility/NoEntityFrameworkTests.cs b/test/NoEntityFrameworkTests/Acceptance/Extensibility/NoEntityFrameworkTests.cs index 95e2f32142..190a8a748c 100644 --- a/test/NoEntityFrameworkTests/Acceptance/Extensibility/NoEntityFrameworkTests.cs +++ b/test/NoEntityFrameworkTests/Acceptance/Extensibility/NoEntityFrameworkTests.cs @@ -4,6 +4,8 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCoreExampleTests.Helpers.Extensions; using Newtonsoft.Json; @@ -37,7 +39,7 @@ public async Task Can_Get_TodoItems() // act var response = await client.SendAsync(request); var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() + var deserializedBody = _fixture.Server.GetService() .DeserializeList(responseBody); // assert @@ -64,7 +66,7 @@ public async Task Can_Get_TodoItems_By_Id() // act var response = await client.SendAsync(request); var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)_fixture.Server.GetService() + var deserializedBody = (TodoItem)_fixture.Server.GetService() .Deserialize(responseBody); // assert @@ -101,7 +103,7 @@ public async Task Can_Create_TodoItems() // act var response = await client.SendAsync(request); var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = (TodoItem)_fixture.Server.GetService() + var deserializedBody = (TodoItem)_fixture.Server.GetService() .Deserialize(responseBody); // assert diff --git a/test/ResourceEntitySeparationExampleTests/Acceptance/GetTests.cs b/test/ResourceEntitySeparationExampleTests/Acceptance/GetTests.cs index 371a07ab9b..ae7da5ad47 100644 --- a/test/ResourceEntitySeparationExampleTests/Acceptance/GetTests.cs +++ b/test/ResourceEntitySeparationExampleTests/Acceptance/GetTests.cs @@ -1,4 +1,6 @@ using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + using JsonApiDotNetCoreExample.Models.Entities; using JsonApiDotNetCoreExample.Models.Resources; using JsonApiDotNetCoreExampleTests.Helpers.Extensions; @@ -32,7 +34,7 @@ public async Task Can_Get_Courses() // act var response = await _fixture.SendAsync("GET", route, null); var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() + var deserializedBody = _fixture.Server.GetService() .DeserializeList(responseBody); // assert @@ -116,7 +118,7 @@ public async Task Can_Get_Departments() // act var response = await _fixture.SendAsync("GET", route, null); var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() + var deserializedBody = _fixture.Server.GetService() .DeserializeList(responseBody); // assert @@ -190,7 +192,7 @@ public async Task Can_Get_Students() // act var response = await _fixture.SendAsync("GET", route, null); var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() + var deserializedBody = _fixture.Server.GetService() .DeserializeList(responseBody); // assert @@ -215,7 +217,7 @@ public async Task Can_Get_Student_By_Id() // act var response = await _fixture.Server.CreateClient().SendAsync(request); var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = (StudentResource)_fixture.Server.GetService() + var deserializedBody = (StudentResource)_fixture.Server.GetService() .Deserialize(responseBody); // assert @@ -249,7 +251,7 @@ public async Task Can_Get_Student_With_Relationships() // act var response = await _fixture.Server.CreateClient().SendAsync(request); var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = (StudentResource)_fixture.Server.GetService() + var deserializedBody = (StudentResource)_fixture.Server.GetService() .Deserialize(responseBody); // assert diff --git a/test/ResourceEntitySeparationExampleTests/Acceptance/RelationshipGetTests.cs b/test/ResourceEntitySeparationExampleTests/Acceptance/RelationshipGetTests.cs index 0beb6c3b6b..acd6ae08c9 100644 --- a/test/ResourceEntitySeparationExampleTests/Acceptance/RelationshipGetTests.cs +++ b/test/ResourceEntitySeparationExampleTests/Acceptance/RelationshipGetTests.cs @@ -1,4 +1,6 @@ using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + using JsonApiDotNetCoreExample.Models.Entities; using JsonApiDotNetCoreExample.Models.Resources; using JsonApiDotNetCoreExampleTests.Helpers.Extensions; @@ -33,7 +35,7 @@ public async Task Can_Get_Courses_For_Department() // act var response = await _fixture.SendAsync("GET", route, null); var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() + var deserializedBody = _fixture.Server.GetService() .DeserializeList(responseBody); // assert @@ -57,7 +59,7 @@ public async Task Can_Get_Course_Relationships_For_Department() // act var response = await _fixture.SendAsync("GET", route, null); var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() + var deserializedBody = _fixture.Server.GetService() .DeserializeList(responseBody); // assert @@ -85,7 +87,7 @@ public async Task Can_Get_Courses_For_Student() // act var response = await _fixture.SendAsync("GET", route, null); var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() + var deserializedBody = _fixture.Server.GetService() .DeserializeList(responseBody); // assert @@ -113,7 +115,7 @@ public async Task Can_Get_Course_Relationships_For_Student() // act var response = await _fixture.SendAsync("GET", route, null); var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() + var deserializedBody = _fixture.Server.GetService() .DeserializeList(responseBody); // assert @@ -184,7 +186,7 @@ public async Task Can_Get_Students_For_Course() // act var response = await _fixture.SendAsync("GET", route, null); var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() + var deserializedBody = _fixture.Server.GetService() .DeserializeList(responseBody); // assert @@ -212,7 +214,7 @@ public async Task Can_Get_Student_Relationships_For_Course() // act var response = await _fixture.SendAsync("GET", route, null); var responseBody = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.Server.GetService() + var deserializedBody = _fixture.Server.GetService() .DeserializeList(responseBody); // assert diff --git a/test/ResourceEntitySeparationExampleTests/TestFixture.cs b/test/ResourceEntitySeparationExampleTests/TestFixture.cs index d54fe43688..2e3617f2b6 100644 --- a/test/ResourceEntitySeparationExampleTests/TestFixture.cs +++ b/test/ResourceEntitySeparationExampleTests/TestFixture.cs @@ -1,5 +1,7 @@ using Bogus; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models.Entities; using JsonApiDotNetCoreExampleTests.Helpers.Extensions; @@ -85,7 +87,7 @@ public async Task SendAsync(string method, string route, ob { var response = await SendAsync(method, route, data); var json = await response.Content.ReadAsStringAsync(); - var obj = (T)Server.GetService().Deserialize(json); + var obj = (T)Server.GetService().Deserialize(json); return (response, obj); } } diff --git a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs index a88ecceb67..186530197a 100644 --- a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs +++ b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs @@ -8,6 +8,7 @@ using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -70,8 +71,7 @@ public void Resources_Without_Names_Specified_Will_Use_Default_Formatter() public void Resources_Without_Names_Specified_Will_Use_Configured_Formatter() { // arrange - JsonApiOptions.ResourceNameFormatter = new CamelCaseNameFormatter(); - var builder = new ResourceGraphBuilder(); + var builder = new ResourceGraphBuilder(new CamelCaseNameFormatter()); builder.AddResource(); // act @@ -101,8 +101,7 @@ public void Attrs_Without_Names_Specified_Will_Use_Default_Formatter() public void Attrs_Without_Names_Specified_Will_Use_Configured_Formatter() { // arrange - JsonApiOptions.ResourceNameFormatter = new CamelCaseNameFormatter(); - var builder = new ResourceGraphBuilder(); + var builder = new ResourceGraphBuilder(new CamelCaseNameFormatter()); builder.AddResource(); // act diff --git a/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs b/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs index 3c5e2e5147..36b5a96dc0 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 DocumentBuilderBehaviour_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 DocumentBuilderOptionsProvider(jsonApiContextMock.Object, httpContextAccessorMock.Object); +// var documentBuilderOptions = sut.GetDocumentBuilderOptions(); - 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 index 00b2cad9a0..0aafaa31e5 100644 --- a/test/UnitTests/Builders/DocumentBuilder_Tests.cs +++ b/test/UnitTests/Builders/DocumentBuilder_Tests.cs @@ -1,411 +1,434 @@ -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); - } - } -} +//using System; +//using System.Collections; +//using System.Collections.Generic; +//using JsonApiDotNetCore.Builders; +//using JsonApiDotNetCore.Configuration; +//using JsonApiDotNetCore.Internal; +//using JsonApiDotNetCore.Internal.Contracts; +//using JsonApiDotNetCore.Managers.Contracts; +//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 IPageManager _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 Mock().Object; +// _jsonApiContextMock +// .Setup(m => m.PageManager) +// .Returns(_pageManager); + + + +// _jsonApiContextMock +// .Setup(m => m.RequestEntity) +// .Returns(_options.ResourceGraph.GetContextEntity(typeof(Model))); +// } + +// [Fact] +// public void Includes_Paging_Links_By_Default() +// { +// // arrange + + +// var rmMock = new Mock(); +// rmMock.Setup(m => m.GetRequestResource()).Returns(new ContextEntity { EntityName = "resources" }); +// var rm = rmMock.Object; +// var options = new JsonApiOptions { RelativeLinks = false }; +// var pg = new PageManager(new LinkBuilder(options, rm), options, rm); +// pg.PageSize = 1; +// pg.TotalRecords = 1; +// pg.CurrentPage = 1; +// var documentBuilder = GetDocumentBuilder(pageManager: pg); +// 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 = GetDocumentBuilder(); +// 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 = GetDocumentBuilder(); +// 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 = GetDocumentBuilder(); +// 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 = GetDocumentBuilder(); +// 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 = GetDocumentBuilder(); +// 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 = GetDocumentBuilder(); + +// 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 = GetDocumentBuilder(); + +// 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 pageManagerMock = new Mock(); +// var requestManagerMock = new Mock(); +// var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, pageManagerMock.Object, requestManagerMock.Object, documentBuilderOptionsProvider: 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 = GetDocumentBuilder(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 = GetDocumentBuilder(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 = GetDocumentBuilder(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 = GetDocumentBuilder(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); +// } +// private DocumentBuilder GetDocumentBuilder(TestScopedServiceProvider scopedServiceProvider = null, IPageManager pageManager = null) +// { +// var pageManagerMock = new Mock(); +// var rmMock = new Mock(); +// rmMock.SetupGet(rm => rm.BasePath).Returns("Localhost"); + +// if (pageManager != null) +// { +// return new DocumentBuilder(_jsonApiContextMock.Object, pageManager, rmMock.Object, scopedServiceProvider: scopedServiceProvider); +// } +// return new DocumentBuilder(_jsonApiContextMock.Object, pageManagerMock.Object, rmMock.Object, scopedServiceProvider: scopedServiceProvider); +// } +// } +//} diff --git a/test/UnitTests/Builders/LinkBuilderTests.cs b/test/UnitTests/Builders/LinkBuilderTests.cs new file mode 100644 index 0000000000..7b8bfea14c --- /dev/null +++ b/test/UnitTests/Builders/LinkBuilderTests.cs @@ -0,0 +1,216 @@ +using JsonApiDotNetCore.Builders; +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 System; + +namespace UnitTests +{ + public class LinkBuilderTests + { + private readonly IPageQueryService _pageManager; + 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() + { + _pageManager = 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); + _provider.Setup(m => m.GetContextEntity("articles")).Returns(GetContextEntity
(resourceLinks: resource)); + var builder = new LinkBuilder(config, GetRequestManager(), _pageManager, _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); + _provider.Setup(m => m.GetContextEntity(typeof(Article))).Returns(GetContextEntity
(relationshipLinks: resource)); + var builder = new LinkBuilder(config, GetRequestManager(), _pageManager, _provider.Object); + var attr = new HasOneAttribute(links: relationship) { DependentType = 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, null)] + [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, null)] + [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, null)] + [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, null)] + public void BuildTopLevelLinks_GlobalAndResourceConfiguration_ExpectedLinks(Link global, + Link resource, + object expectedSelfLink, + bool pages) + { + // arrange + var config = GetConfiguration(topLevelLinks: global); + var resourceContext = GetContextEntity
(topLevelLinks: resource); + var builder = new LinkBuilder(config, GetRequestManager(resourceContext), _pageManager, null); + + // act + var links = builder.GetTopLevelLinks(); + + // 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 IRequestContext GetRequestManager(ContextEntity resourceContext = null) + { + var mock = new Mock(); + mock.Setup(m => m.BasePath).Returns(_host); + mock.Setup(m => m.GetRequestResource()).Returns(resourceContext); + return mock.Object; + } + + private IGlobalLinksConfiguration 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 IPageQueryService 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 ContextEntity GetContextEntity(Link resourceLinks = Link.NotConfigured, + Link topLevelLinks = Link.NotConfigured, + Link relationshipLinks = Link.NotConfigured) where TResource : class, IIdentifiable + { + return new ContextEntity + { + ResourceLinks = resourceLinks, + TopLevelLinks = topLevelLinks, + RelationshipLinks = relationshipLinks, + EntityName = 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..4dc3dba47f --- /dev/null +++ b/test/UnitTests/Builders/LinkTests.cs @@ -0,0 +1,39 @@ +using System; +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..c0cf81d4d3 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..14bf06e3d5 100644 --- a/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs +++ b/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs @@ -8,6 +8,8 @@ using JsonApiDotNetCore.Internal; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using JsonApiDotNetCore.Internal.Contracts; +using System.IO; namespace UnitTests { @@ -17,7 +19,7 @@ public class Resource : Identifiable { [Attr("test-attribute")] public string TestAttribute { get; set; } } - private Mock _jsonApiContextMock = new Mock(); + private Mock _resourceGraph = new Mock(); private Mock _resourceGraphMock = new Mock(); [Fact] @@ -25,14 +27,14 @@ public async Task GetAsync_Calls_Service() { // arrange var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getAll: serviceMock.Object); + var controller = new BaseJsonApiController(new Mock().Object, _resourceGraph.Object, getAll: serviceMock.Object); // act await controller.GetAsync(); // assert serviceMock.Verify(m => m.GetAsync(), Times.Once); - VerifyApplyContext(); + } [Fact] @@ -40,7 +42,7 @@ public async Task GetAsync_Throws_405_If_No_Service() { // arrange var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, null); + var controller = new BaseJsonApiController(new Mock().Object, _resourceGraph.Object, null); // act var exception = await Assert.ThrowsAsync(() => controller.GetAsync()); @@ -55,14 +57,14 @@ public async Task GetAsyncById_Calls_Service() // 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, _resourceGraph.Object, getById: serviceMock.Object); // act await controller.GetAsync(id); // assert serviceMock.Verify(m => m.GetAsync(id), Times.Once); - VerifyApplyContext(); + } [Fact] @@ -71,7 +73,7 @@ public async Task GetAsyncById_Throws_405_If_No_Service() // arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getById: null); + var controller = new BaseJsonApiController(new Mock().Object, _resourceGraph.Object, getById: null); // act var exception = await Assert.ThrowsAsync(() => controller.GetAsync(id)); @@ -86,14 +88,13 @@ public async Task GetRelationshipsAsync_Calls_Service() // 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, _resourceGraph.Object, getRelationships: serviceMock.Object); // act await controller.GetRelationshipsAsync(id, string.Empty); // assert serviceMock.Verify(m => m.GetRelationshipsAsync(id, string.Empty), Times.Once); - VerifyApplyContext(); } [Fact] @@ -102,7 +103,7 @@ public async Task GetRelationshipsAsync_Throws_405_If_No_Service() // arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationships: null); + var controller = new BaseJsonApiController(new Mock().Object, _resourceGraph.Object, getRelationships: null); // act var exception = await Assert.ThrowsAsync(() => controller.GetRelationshipsAsync(id, string.Empty)); @@ -117,14 +118,13 @@ public async Task GetRelationshipAsync_Calls_Service() // 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, _resourceGraph.Object, getRelationship: serviceMock.Object); // act await controller.GetRelationshipAsync(id, string.Empty); // assert serviceMock.Verify(m => m.GetRelationshipAsync(id, string.Empty), Times.Once); - VerifyApplyContext(); } [Fact] @@ -133,7 +133,7 @@ public async Task GetRelationshipAsync_Throws_405_If_No_Service() // arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationship: null); + var controller = new BaseJsonApiController(new Mock().Object, _resourceGraph.Object, getRelationship: null); // act var exception = await Assert.ThrowsAsync(() => controller.GetRelationshipAsync(id, string.Empty)); @@ -149,16 +149,16 @@ public async Task PatchAsync_Calls_Service() 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); + //_resourceGraph.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_resourceGraph.Object); + + + var controller = new BaseJsonApiController(new JsonApiOptions(), _resourceGraph.Object, update: serviceMock.Object); // act await controller.PatchAsync(id, resource); // assert serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny()), Times.Once); - VerifyApplyContext(); } [Fact] @@ -168,16 +168,15 @@ public async Task PatchAsync_ModelStateInvalid_ValidateModelStateDisbled() 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); + //_resourceGraph.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_resourceGraph.Object); + + var controller = new BaseJsonApiController(new JsonApiOptions(), _resourceGraph.Object, update: serviceMock.Object); // act var response = await controller.PatchAsync(id, resource); // assert serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny()), Times.Once); - VerifyApplyContext(); Assert.IsNotType(response); } @@ -188,11 +187,10 @@ public async Task PatchAsync_ModelStateInvalid_ValidateModelStateEnabled() 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); +// _resourceGraph.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_resourceGraph.Object); + + var controller = new BaseJsonApiController(new JsonApiOptions { ValidateModelState = true }, _resourceGraph.Object, update: serviceMock.Object); controller.ModelState.AddModelError("TestAttribute", "Failed Validation"); // act @@ -210,7 +208,7 @@ public async Task PatchAsync_Throws_405_If_No_Service() // arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: null); + var controller = new BaseJsonApiController(new Mock().Object, _resourceGraph.Object, update: null); // act var exception = await Assert.ThrowsAsync(() => controller.PatchAsync(id, It.IsAny())); @@ -225,9 +223,9 @@ public async Task PostAsync_Calls_Service() // 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); +// _resourceGraph.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_resourceGraph.Object); + + var controller = new BaseJsonApiController(new JsonApiOptions(), _resourceGraph.Object, create: serviceMock.Object); serviceMock.Setup(m => m.CreateAsync(It.IsAny())).ReturnsAsync(resource); controller.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext {HttpContext = new DefaultHttpContext()}; @@ -236,7 +234,6 @@ public async Task PostAsync_Calls_Service() // assert serviceMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Once); - VerifyApplyContext(); } [Fact] @@ -245,18 +242,16 @@ public async Task PostAsync_ModelStateInvalid_ValidateModelStateDisabled() // 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 }, _resourceGraph.Object, create: serviceMock.Object); controller.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext { HttpContext = new DefaultHttpContext() }; + serviceMock.Setup(m => m.CreateAsync(It.IsAny())).ReturnsAsync(resource); + // act var response = await controller.PostAsync(resource); // assert serviceMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Once); - VerifyApplyContext(); Assert.IsNotType(response); } @@ -266,12 +261,12 @@ public async Task PostAsync_ModelStateInvalid_ValidateModelStateEnabled() // 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 }, _resourceGraph.Object, create: serviceMock.Object); + controller.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext { HttpContext = new DefaultHttpContext() }; controller.ModelState.AddModelError("TestAttribute", "Failed Validation"); + serviceMock.Setup(m => m.CreateAsync(It.IsAny())).ReturnsAsync(resource); + // act var response = await controller.PostAsync(resource); @@ -289,14 +284,13 @@ public async Task PatchRelationshipsAsync_Calls_Service() 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, _resourceGraph.Object, updateRelationships: serviceMock.Object); // act await controller.PatchRelationshipsAsync(id, string.Empty, null); // assert serviceMock.Verify(m => m.UpdateRelationshipsAsync(id, string.Empty, null), Times.Once); - VerifyApplyContext(); } [Fact] @@ -305,7 +299,7 @@ public async Task PatchRelationshipsAsync_Throws_405_If_No_Service() // arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, updateRelationships: null); + var controller = new BaseJsonApiController(new Mock().Object, _resourceGraph.Object, updateRelationships: null); // act var exception = await Assert.ThrowsAsync(() => controller.PatchRelationshipsAsync(id, string.Empty, null)); @@ -317,18 +311,17 @@ public async Task PatchRelationshipsAsync_Throws_405_If_No_Service() [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, _resourceGraph.Object, delete: serviceMock.Object); - // act + // Act await controller.DeleteAsync(id); - // assert + // Assert serviceMock.Verify(m => m.DeleteAsync(id), Times.Once); - VerifyApplyContext(); } [Fact] @@ -337,7 +330,9 @@ public async Task DeleteAsync_Throws_405_If_No_Service() // arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, delete: null); + var controller = new BaseJsonApiController(new Mock().Object, + + _resourceGraph.Object, delete: null); // act var exception = await Assert.ThrowsAsync(() => controller.DeleteAsync(id)); @@ -346,7 +341,6 @@ public async Task DeleteAsync_Throws_405_If_No_Service() Assert.Equal(405, exception.GetStatusCode()); } - private void VerifyApplyContext() - => _jsonApiContextMock.Verify(m => m.ApplyContext(It.IsAny>()), Times.Once); + } } diff --git a/test/UnitTests/Data/DefaultEntityRepository_Tests.cs b/test/UnitTests/Data/DefaultEntityRepository_Tests.cs index 97cf51d587..9e14d173d9 100644 --- a/test/UnitTests/Data/DefaultEntityRepository_Tests.cs +++ b/test/UnitTests/Data/DefaultEntityRepository_Tests.cs @@ -1,187 +1,179 @@ -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); +//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; + +//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.RequestManager.GetUpdatedAttributes()) +// .Returns(_attrsToUpdate); + +// _jsonApiContextMock +// .Setup(m => m.RequestManager.GetUpdatedRelationships()) +// .Returns(_relationshipsToUpdate); + + +// 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; - } - } -} +// 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/Extensions/IServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index 3b49dda9bf..47e5807c02 100644 --- a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -6,6 +6,8 @@ using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Contracts; + using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -17,6 +19,9 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Serialization.Contracts; +using JsonApiDotNetCore.Managers.Contracts; namespace UnitTests.Extensions { @@ -30,7 +35,7 @@ public void AddJsonApiInternals_Adds_All_Required_Services() var jsonApiOptions = new JsonApiOptions(); services.AddDbContext(options => options.UseInMemoryDatabase("UnitTestDb"), ServiceLifetime.Transient); - + services.AddScoped>(); // act services.AddJsonApiInternals(jsonApiOptions); // this is required because the DbContextResolver requires access to the current HttpContext @@ -39,20 +44,24 @@ public void AddJsonApiInternals_Adds_All_Required_Services() var provider = services.BuildServiceProvider(); // assert + var requestManager = provider.GetService(); + Assert.NotNull(requestManager); + var graph = provider.GetService(); + Assert.NotNull(graph); + requestManager.SetRequestResource(graph.GetContextEntity()); + Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService(typeof(IEntityRepository))); - 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()); Assert.NotNull(provider.GetService()); - Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService(typeof(GenericProcessor))); } diff --git a/test/UnitTests/JsonApiContext/BasicTest.cs b/test/UnitTests/JsonApiContext/BasicTest.cs new file mode 100644 index 0000000000..218b69327d --- /dev/null +++ b/test/UnitTests/JsonApiContext/BasicTest.cs @@ -0,0 +1,102 @@ +using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreExample.Controllers; +using JsonApiDotNetCoreExample.Models; +using Moq; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Microsoft.AspNetCore.Mvc; +using JsonApiDotNetCoreExample.Services; +using JsonApiDotNetCore.Data; +using Microsoft.Extensions.Logging; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal; +using System.Net; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Internal.Query; +using System.Linq; + +namespace UnitTests.Services +{ + public class EntityResourceServiceMore + { + [Fact] + public async Task TestCanGetAll() + { + + } + + /// + /// we expect the service layer to give use a 404 if there is no entity returned + /// + /// + [Fact] + public async Task GetAsync_Throw404OnNoEntityFound() + { + // Arrange + var jacMock = FetchContextMock(); + var loggerMock = new Mock(); + var jsonApiOptions = new JsonApiOptions + { + IncludeTotalRecordCount = false + } as IJsonApiOptions; + var repositoryMock = new Mock>(); + var queryManagerMock = new Mock(); + var pageManagerMock = new Mock(); + var rgMock = new Mock(); + var service = new CustomArticleService(repositoryMock.Object, jsonApiOptions, queryManagerMock.Object, pageManagerMock.Object, rgMock.Object); + + // Act / Assert + var toExecute = new Func(() => + { + return service.GetAsync(4); + }); + var exception = await Assert.ThrowsAsync(toExecute); + Assert.Equal(404, exception.GetStatusCode()); + } + + /// + /// we expect the service layer to give use a 404 if there is no entity returned + /// + /// + [Fact] + public async Task GetAsync_ShouldThrow404OnNoEntityFoundWithRelationships() + { + // Arrange + var jacMock = FetchContextMock(); + var loggerMock = new Mock(); + var jsonApiOptions = new JsonApiOptions + { + IncludeTotalRecordCount = false + } as IJsonApiOptions; + var repositoryMock = new Mock>(); + + var requestManager = new Mock(); + var pageManagerMock = new Mock(); + requestManager.Setup(qm => qm.GetRelationships()).Returns(new List() { "cookies" }); + requestManager.SetupGet(rm => rm.QuerySet).Returns(new QuerySet + { + IncludedRelationships = new List { "cookies" } + }); + var rgMock = new Mock(); + var service = new CustomArticleService(repositoryMock.Object, jsonApiOptions, requestManager.Object, pageManagerMock.Object, rgMock.Object); + + // Act / Assert + var toExecute = new Func(() => + { + return service.GetAsync(4); + }); + var exception = await Assert.ThrowsAsync(toExecute); + Assert.Equal(404, exception.GetStatusCode()); + } + + public Mock FetchContextMock() + { + return new Mock(); + } + + } +} diff --git a/test/UnitTests/Models/LinkTests.cs b/test/UnitTests/Models/LinkTests.cs index e954ddf135..88f56a4a6d 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 diff --git a/test/UnitTests/Models/RelationshipDataTests.cs b/test/UnitTests/Models/RelationshipDataTests.cs index ff00144b62..e4871ad70d 100644 --- a/test/UnitTests/Models/RelationshipDataTests.cs +++ b/test/UnitTests/Models/RelationshipDataTests.cs @@ -8,7 +8,7 @@ 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(); @@ -20,17 +20,17 @@ public void Setting_ExposedData_To_List_Sets_ManyData() }; // act - relationshipData.ExposedData = relationships; + relationshipData.Data = relationships; // 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(); @@ -44,17 +44,17 @@ public void Setting_ExposedData_To_JArray_Sets_ManyData() var relationships = JArray.Parse(relationshipsJson); // act - relationshipData.ExposedData = relationships; + relationshipData.Data = relationships; // 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(); @@ -64,17 +64,17 @@ public void Setting_ExposedData_To_RIO_Sets_SingleData() }; // act - relationshipData.ExposedData = relationship; + relationshipData.Data = relationship; // 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(); @@ -86,13 +86,13 @@ public void Setting_ExposedData_To_JObject_Sets_SingleData() var relationship = JObject.Parse(relationshipJson); // act - relationshipData.ExposedData = relationship; + relationshipData.Data = relationship; // 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..776d795da6 100644 --- a/test/UnitTests/Models/ResourceDefinitionTests.cs +++ b/test/UnitTests/Models/ResourceDefinitionTests.cs @@ -1,145 +1,146 @@ -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Query; -using JsonApiDotNetCore.Models; -using System.Collections.Generic; -using System.Linq; -using Xunit; - -namespace UnitTests.Models -{ - public class ResourceDefinition_Scenario_Tests - { - private readonly IResourceGraph _graph; - - public ResourceDefinition_Scenario_Tests() - { - _graph = new ResourceGraphBuilder() - .AddResource("models") - .Build(); - } - - [Fact] - public void Request_Filter_Uses_Member_Expression() - { - // arrange - var resource = new RequestFilteredResource(isAdmin: true); - - // act - var attrs = resource.GetOutputAttrs(null); - - // assert - Assert.DoesNotContain(attrs, a => a.InternalAttributeName == nameof(Model.AlwaysExcluded)); - } - - [Fact] - public void Request_Filter_Uses_NewExpression() - { - // arrange - var resource = new RequestFilteredResource(isAdmin: false); - - // act - var attrs = resource.GetOutputAttrs(null); - - // 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 - { - [Attr("name")] public string AlwaysExcluded { get; set; } - [Attr("password")] public string Password { get; set; } - [Attr("prop")] public string Prop { get; set; } - } - - 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()) - { - _isAdmin = isAdmin; - } - - // 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) } - }; - public override PropertySortOrder GetDefaultSortOrder() - => new PropertySortOrder { - (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 +//using JsonApiDotNetCore.Builders; +//using JsonApiDotNetCore.Internal; +//using JsonApiDotNetCore.Internal.Contracts; +//using JsonApiDotNetCore.Internal.Query; +//using JsonApiDotNetCore.Models; +//using System.Collections.Generic; +//using System.Linq; +//using Xunit; + +//namespace UnitTests.Models +//{ +// public class ResourceDefinition_Scenario_Tests +// { +// private readonly IResourceGraph _graph; + +// public ResourceDefinition_Scenario_Tests() +// { +// _graph = new ResourceGraphBuilder() +// .AddResource("models") +// .Build(); +// } + +// [Fact] +// public void Request_Filter_Uses_Member_Expression() +// { +// // arrange +// var resource = new RequestFilteredResource(isAdmin: true); + +// // act +// var attrs = resource.GetOutputAttrs(null); + +// // assert +// Assert.DoesNotContain(attrs, a => a.InternalAttributeName == nameof(Model.AlwaysExcluded)); +// } + +// [Fact] +// public void Request_Filter_Uses_NewExpression() +// { +// // arrange +// var resource = new RequestFilteredResource(isAdmin: false); + +// // act +// var attrs = resource.GetOutputAttrs(null); + +// // 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 +// { +// [Attr("name")] public string AlwaysExcluded { get; set; } +// [Attr("password")] public string Password { get; set; } +// [Attr("prop")] public string Prop { get; set; } +// } + +// 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()) +// { +// _isAdmin = isAdmin; +// } + +// // 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) } +// }; +// public override PropertySortOrder GetDefaultSortOrder() +// => new PropertySortOrder { +// (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/ResourceHooks/DiscoveryTests.cs b/test/UnitTests/ResourceHooks/DiscoveryTests.cs index bde024f530..fe6d798c37 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 { @@ -60,11 +61,11 @@ 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(() => diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/AfterCreateTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/AfterCreateTests.cs index dea114facc..80f3966d60 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/AfterCreateTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/AfterCreateTests.cs @@ -16,8 +16,7 @@ public void AfterCreate() // 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 @@ -35,8 +34,7 @@ public void AfterCreate_Without_Parent_Hook_Implemented() // 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 @@ -53,8 +51,7 @@ public void AfterCreate_Without_Child_Hook_Implemented() // 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 @@ -71,8 +68,7 @@ public void AfterCreate_Without_Any_Hook_Implemented() // 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 diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreateTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreateTests.cs index bc2163df2f..a8370067f8 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreateTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreateTests.cs @@ -17,8 +17,7 @@ public void BeforeCreate() 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 @@ -36,8 +35,7 @@ public void BeforeCreate_Without_Parent_Hook_Implemented() 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 @@ -54,8 +52,7 @@ public void BeforeCreate_Without_Child_Hook_Implemented() 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 @@ -71,8 +68,7 @@ public void BeforeCreate_Without_Any_Hook_Implemented() 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 diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreate_WithDbValues_Tests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreate_WithDbValues_Tests.cs index c574de145c..667f259591 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreate_WithDbValues_Tests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreate_WithDbValues_Tests.cs @@ -47,8 +47,7 @@ public void BeforeCreate() // 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 hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); @@ -73,8 +72,7 @@ public void BeforeCreate_Without_Parent_Hook_Implemented() // 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 hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); @@ -94,8 +92,7 @@ public void BeforeCreate_Without_Child_Hook_Implemented() // 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 hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); @@ -115,8 +112,7 @@ public void BeforeCreate_NoImplicit() // 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 hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); @@ -137,8 +133,7 @@ public void BeforeCreate_NoImplicit_Without_Parent_Hook_Implemented() // 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 hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); @@ -158,8 +153,7 @@ public void BeforeCreate_NoImplicit_Without_Child_Hook_Implemented() // 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 hookExecutor.BeforeCreate(todoList, ResourcePipeline.Post); diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/AfterDeleteTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/AfterDeleteTests.cs index 051fea3bba..65c8cb4a54 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); } @@ -31,7 +31,7 @@ public void AfterDelete_Without_Any_Hook_Implemented() { // 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 diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDeleteTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDeleteTests.cs index 887a322994..15b1c247b1 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDeleteTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDeleteTests.cs @@ -15,7 +15,7 @@ public void BeforeDelete() { // 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 @@ -31,7 +31,7 @@ public void BeforeDelete_Without_Any_Hook_Implemented() { // 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 diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDelete_WithDbValue_Tests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDelete_WithDbValue_Tests.cs index f63adcbd6e..0dc09d7b3d 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDelete_WithDbValue_Tests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDelete_WithDbValue_Tests.cs @@ -39,8 +39,7 @@ public void BeforeDelete() 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 @@ -48,8 +47,8 @@ public void BeforeDelete() // 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); } @@ -60,8 +59,7 @@ public void BeforeDelete_No_Parent_Hooks() 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 @@ -80,8 +78,7 @@ public void BeforeDelete_No_Children_Hooks() 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 diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/IdentifiableManyToMany_OnReturnTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/IdentifiableManyToMany_OnReturnTests.cs index fd29857c37..cc0f89b4a8 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/IdentifiableManyToMany_OnReturnTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/IdentifiableManyToMany_OnReturnTests.cs @@ -18,9 +18,8 @@ public void OnReturn() 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 hookExecutor.OnReturn(articles, ResourcePipeline.Get); @@ -39,9 +38,8 @@ public void OnReturn_GetRelationship() 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 hookExecutor.OnReturn(articles, ResourcePipeline.GetRelationship); @@ -59,9 +57,8 @@ public void OnReturn_Without_Parent_Hook_Implemented() 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 hookExecutor.OnReturn(articles, ResourcePipeline.Get); @@ -79,10 +76,9 @@ public void OnReturn_Without_Children_Hooks_Implemented() 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 hookExecutor.OnReturn(articles, ResourcePipeline.Get); @@ -100,9 +96,8 @@ public void OnReturn_Without_Grand_Children_Hooks_Implemented() 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 hookExecutor.OnReturn(articles, ResourcePipeline.Get); @@ -120,9 +115,8 @@ public void OnReturn_Without_Any_Descendant_Hooks_Implemented() 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 hookExecutor.OnReturn(articles, ResourcePipeline.Get); @@ -139,9 +133,8 @@ public void OnReturn_Without_Any_Hook_Implemented() 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 hookExecutor.OnReturn(articles, ResourcePipeline.Get); diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/ManyToMany_OnReturnTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/ManyToMany_OnReturnTests.cs index ad9577c3b5..88326d3994 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/ManyToMany_OnReturnTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/ManyToMany_OnReturnTests.cs @@ -47,9 +47,8 @@ public void OnReturn() // 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 hookExecutor.OnReturn(articles, ResourcePipeline.Get); @@ -66,9 +65,8 @@ public void OnReturn_Without_Parent_Hook_Implemented() // 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 hookExecutor.OnReturn(articles, ResourcePipeline.Get); @@ -84,9 +82,8 @@ public void OnReturn_Without_Children_Hooks_Implemented() // 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 hookExecutor.OnReturn(articles, ResourcePipeline.Get); @@ -102,10 +99,9 @@ public void OnReturn_Without_Any_Hook_Implemented() // 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 hookExecutor.OnReturn(articles, ResourcePipeline.Get); diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/BeforeReadTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/BeforeReadTests.cs index e0eceb2fd5..08d940bb3a 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; @@ -16,10 +17,10 @@ public void BeforeRead() // 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()); + iqMock.Setup(c => c.Get()).Returns(new List>()); // act hookExecutor.BeforeRead(ResourcePipeline.Get); // assert @@ -35,12 +36,11 @@ public void BeforeReadWithInclusion() 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 hookExecutor.BeforeRead(ResourcePipeline.Get); @@ -58,12 +58,11 @@ public void BeforeReadWithNestedInclusion() 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 hookExecutor.BeforeRead(ResourcePipeline.Get); @@ -83,12 +82,11 @@ public void BeforeReadWithNestedInclusion_No_Parent_Hook_Implemented() 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 hookExecutor.BeforeRead(ResourcePipeline.Get); @@ -106,12 +104,11 @@ public void BeforeReadWithNestedInclusion_No_Child_Hook_Implemented() 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 hookExecutor.BeforeRead(ResourcePipeline.Get); @@ -129,12 +126,11 @@ public void BeforeReadWithNestedInclusion_No_Grandchild_Hook_Implemented() 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 hookExecutor.BeforeRead(ResourcePipeline.Get); @@ -153,12 +149,11 @@ public void BeforeReadWithNestedInclusion_Without_Any_Hook_Implemented() 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 hookExecutor.BeforeRead(ResourcePipeline.Get); diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/IdentifiableManyToMany_AfterReadTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/IdentifiableManyToMany_AfterReadTests.cs index 1d34524029..ac58056ca5 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/IdentifiableManyToMany_AfterReadTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/IdentifiableManyToMany_AfterReadTests.cs @@ -18,9 +18,8 @@ public void AfterRead() 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 hookExecutor.AfterRead(articles, ResourcePipeline.Get); @@ -39,9 +38,8 @@ public void AfterRead_Without_Parent_Hook_Implemented() 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 hookExecutor.AfterRead(articles, ResourcePipeline.Get); @@ -60,10 +58,9 @@ public void AfterRead_Without_Children_Hooks_Implemented() 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 hookExecutor.AfterRead(articles, ResourcePipeline.Get); @@ -81,9 +78,8 @@ public void AfterRead_Without_Grand_Children_Hooks_Implemented() 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 hookExecutor.AfterRead(articles, ResourcePipeline.Get); @@ -101,9 +97,8 @@ public void AfterRead_Without_Any_Descendant_Hooks_Implemented() 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 hookExecutor.AfterRead(articles, ResourcePipeline.Get); @@ -120,9 +115,8 @@ public void AfterRead_Without_Any_Hook_Implemented() 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 hookExecutor.AfterRead(articles, ResourcePipeline.Get); diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/ManyToMany_AfterReadTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/ManyToMany_AfterReadTests.cs index dcdf81ef94..7f16620469 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/ManyToMany_AfterReadTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/ManyToMany_AfterReadTests.cs @@ -17,9 +17,8 @@ public void AfterRead() // 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 hookExecutor.AfterRead(articles, ResourcePipeline.Get); @@ -36,9 +35,8 @@ public void AfterRead_Without_Parent_Hook_Implemented() // 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 hookExecutor.AfterRead(articles, ResourcePipeline.Get); @@ -54,9 +52,8 @@ public void AfterRead_Without_Children_Hooks_Implemented() // 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 hookExecutor.AfterRead(articles, ResourcePipeline.Get); @@ -72,9 +69,8 @@ public void AfterRead_Without_Any_Hook_Implemented() // 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 hookExecutor.AfterRead(articles, ResourcePipeline.Get); diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/ScenarioTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/ScenarioTests.cs index c79f633c83..a4f84892ed 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/ScenarioTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/ScenarioTests.cs @@ -16,8 +16,7 @@ public void Entity_Has_Multiple_Relations_To_Same_Type() // 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 } }; @@ -40,7 +39,7 @@ public void Entity_Has_Cyclic_Relations() { // 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 }; @@ -66,8 +65,8 @@ 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 diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/AfterUpdateTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/AfterUpdateTests.cs index e8d4f4fd60..c20176b6a2 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/AfterUpdateTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/AfterUpdateTests.cs @@ -16,8 +16,7 @@ public void AfterUpdate() // 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 @@ -35,8 +34,7 @@ public void AfterUpdate_Without_Parent_Hook_Implemented() // 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 @@ -53,8 +51,7 @@ public void AfterUpdate_Without_Child_Hook_Implemented() // 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 @@ -71,8 +68,7 @@ public void AfterUpdate_Without_Any_Hook_Implemented() // 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 diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdateTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdateTests.cs index 807dc38b18..efc20d313f 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdateTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdateTests.cs @@ -16,8 +16,7 @@ public void BeforeUpdate() // 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 @@ -35,8 +34,7 @@ public void BeforeUpdate_Without_Parent_Hook_Implemented() // 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 @@ -54,8 +52,7 @@ public void BeforeUpdate_Without_Child_Hook_Implemented() 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 @@ -72,8 +69,7 @@ public void BeforeUpdate_Without_Any_Hook_Implemented() // 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 diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdate_WithDbValues_Tests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdate_WithDbValues_Tests.cs index a13de78f94..1210015951 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; @@ -52,8 +50,7 @@ public void BeforeUpdate() // 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 hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); @@ -83,10 +80,9 @@ public void BeforeUpdate_Deleting_Relationship() // 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); + + ufMock.Setup(c => c.Relationshipss)).Returns(_fieldExplorer.GetRelationships((TodoItem t) => t.ToOnePerson)); // act var _todoList = new List() { new TodoItem { Id = this.todoList[0].Id } }; @@ -108,8 +104,7 @@ public void BeforeUpdate_Without_Parent_Hook_Implemented() // 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 hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); @@ -133,8 +128,7 @@ public void BeforeUpdate_Without_Child_Hook_Implemented() // 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 hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); @@ -154,8 +148,7 @@ public void BeforeUpdate_NoImplicit() // 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 hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); @@ -176,8 +169,7 @@ public void BeforeUpdate_NoImplicit_Without_Parent_Hook_Implemented() // 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 hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); @@ -197,8 +189,7 @@ public void BeforeUpdate_NoImplicit_Without_Child_Hook_Implemented() // 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 hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); diff --git a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs index 454a2e225f..82b3cb005c 100644 --- a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs +++ b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs @@ -15,11 +15,16 @@ using System.Collections.Generic; using System.Linq; using Person = JsonApiDotNetCoreExample.Models.Person; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Internal.Query; namespace UnitTests.ResourceHooks { public class HooksDummyData { + protected IExposedFieldExplorer _fieldExplorer; + protected IResourceGraph _graph; protected ResourceHook[] NoHooks = new ResourceHook[0]; protected ResourceHook[] EnableDbValues = { ResourceHook.BeforeUpdate, ResourceHook.BeforeUpdateRelationship }; protected ResourceHook[] DisableDbValues = new ResourceHook[0]; @@ -32,7 +37,7 @@ public class HooksDummyData protected readonly Faker _passportFaker; public HooksDummyData() { - new ResourceGraphBuilder() + _graph = new ResourceGraphBuilder() .AddResource() .AddResource() .AddResource() @@ -42,6 +47,8 @@ public HooksDummyData() .AddResource() .Build(); + _fieldExplorer = new ExposedFieldExplorer(_graph); + _todoFaker = new Faker().Rules((f, i) => i.Id = f.UniqueIndex + 1); _personFaker = new Faker().Rules((f, i) => i.Id = f.UniqueIndex + 1); @@ -134,57 +141,68 @@ protected List CreateTodoWithOwner() public class HooksTestsSetup : HooksDummyData { - protected (Mock, IResourceHookExecutor, Mock>) - CreateTestObjects(IHooksDiscovery discovery = null) - where TMain : class, IIdentifiable + (IResourceGraph, Mock, Mock, Mock, IJsonApiOptions) CreateMocks() + { + var pfMock = new Mock(); + var graph = _graph; + var ufMock = new Mock(); + var iqsMock = new Mock(); + var optionsMock = new JsonApiOptions { LoaDatabaseValues = false }; + return (graph, 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 GenericProcessorFactory and JsonApiContext and wiring them up. - (var context, var processorFactory) = CreateContextAndProcessorMocks(); + var (graph, ufMock, iqMock, gpfMock, options) = CreateMocks(); + + 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, graph, options); + var traversalHelper = new TraversalHelper(graph, ufMock.Object); + var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, iqMock.Object, graph); - 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(); + var (graph, 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, graph, options); + var traversalHelper = new TraversalHelper(graph, ufMock.Object); + var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, iqMock.Object, graph); - 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 @@ -196,18 +214,19 @@ public class HooksTestsSetup : HooksDummyData var secondNestedResource = CreateResourceDefinition(secondNestedDiscovery); // mocking the GenericProcessorFactory and JsonApiContext and wiring them up. - (var context, var processorFactory) = CreateContextAndProcessorMocks(); + var (graph, 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, graph, options); + var traversalHelper = new TraversalHelper(graph, ufMock.Object); + var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, iqMock.Object, graph); - return (context, hookExecutor, mainResource, firstNestedResource, secondNestedResource); + return (iqMock, hookExecutor, mainResource, firstNestedResource, secondNestedResource); } protected IHooksDiscovery SetDiscoverableHooks(ResourceHook[] implementedHooks, params ResourceHook[] enableDbValuesHooks) @@ -239,8 +258,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)) { @@ -297,7 +316,7 @@ void MockHooks(Mock> resourceDefinition) 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.Options).Returns(new JsonApiOptions { LoaDatabaseValues = false }); context.Setup(c => c.ResourceGraph).Returns(ResourceGraph.Instance); return (context, processorFactory); @@ -307,7 +326,6 @@ void SetupProcessorFactoryForResourceDefinition( Mock processorFactory, IResourceHookContainer modelResource, IHooksDiscovery discovery, - IJsonApiContext apiContext, AppDbContext dbContext = null ) where TModel : class, IIdentifiable @@ -323,9 +341,10 @@ void SetupProcessorFactoryForResourceDefinition( var idType = TypeHelper.GetIdentifierType(); if (idType == typeof(int)) { - IEntityReadRepository repo = CreateTestRepository(dbContext, apiContext); + IEntityReadRepository repo = CreateTestRepository(dbContext, new Mock().Object); processorFactory.Setup(c => c.GetProcessor>(typeof(IEntityReadRepository<,>), typeof(TModel), typeof(int))).Returns(repo); - } else + } + else { throw new TypeLoadException("Test not set up properly"); } @@ -339,7 +358,7 @@ IJsonApiContext apiContext ) where TModel : class, IIdentifiable { IDbContextResolver resolver = CreateTestDbResolver(dbContext); - return new DefaultEntityRepository(apiContext, resolver); + return new DefaultEntityRepository(null, apiContext, resolver); } IDbContextResolver CreateTestDbResolver(AppDbContext dbContext) where TModel : class, IIdentifiable @@ -363,6 +382,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 = _graph.GetContextEntity(); + var splittedPath = chain.Split(QueryConstants.DOT); + foreach (var requestedRelationship in splittedPath) + { + var relationship = resourceContext.Relationships.Single(r => r.PublicRelationshipName == requestedRelationship); + parsedChain.Add(relationship); + resourceContext = _graph.GetContextEntity(relationship.DependentType); + } + return parsedChain; + } } } 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/Deserializer/ClientDeserializerTests.cs b/test/UnitTests/Serialization/Deserializer/ClientDeserializerTests.cs new file mode 100644 index 0000000000..c76500d8d4 --- /dev/null +++ b/test/UnitTests/Serialization/Deserializer/ClientDeserializerTests.cs @@ -0,0 +1,335 @@ +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.Links; +using JsonApiDotNetCore.Serialization; +using Newtonsoft.Json; +using Xunit; + +namespace UnitTests.Serialization.Deserializer +{ + public class ClientDeserializerTests : DeserializerTestsSetup + { + private readonly Dictionary _linkValues = new Dictionary(); + private readonly ClientDeserializer _deserializer; + + public ClientDeserializerTests() + { + _deserializer = new ClientDeserializer(_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); + Assert.Equal(_linkValues["self"], result.Links.Self); + Assert.Equal(_linkValues["next"], result.Links.Next); + Assert.Equal(_linkValues["last"], result.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); + Assert.Equal(_linkValues["self"], result.Links.Self); + Assert.Equal(_linkValues["next"], result.Links.Next); + Assert.Equal(_linkValues["last"], result.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/Deserializer/DeserializerTestsSetup.cs b/test/UnitTests/Serialization/Deserializer/DeserializerTestsSetup.cs new file mode 100644 index 0000000000..35ece88f97 --- /dev/null +++ b/test/UnitTests/Serialization/Deserializer/DeserializerTestsSetup.cs @@ -0,0 +1,79 @@ +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization; +using System.Collections.Generic; + +namespace UnitTests.Serialization.Deserializer +{ + public class DeserializerTestsSetup : SerializationTestsSetupBase + { + protected class TestDocumentParser : DocumentParser + { + public TestDocumentParser(IResourceGraph resourceGraph) : base(resourceGraph) { } + + public new object Deserialize(string body) + { + return base.Deserialize(body); + } + + protected override void AfterProcessField(IIdentifiable entity, IResourceField field, RelationshipData 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 RelationshipData CreateRelationshipData(string relatedType = null, bool isToManyData = false) + { + var data = new RelationshipData(); + 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/Deserializer/DocumentParserTests.cs b/test/UnitTests/Serialization/Deserializer/DocumentParserTests.cs new file mode 100644 index 0000000000..80ed40183b --- /dev/null +++ b/test/UnitTests/Serialization/Deserializer/DocumentParserTests.cs @@ -0,0 +1,372 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization; +using Newtonsoft.Json; +using Xunit; + +namespace UnitTests.Serialization.Deserializer +{ + public class DocumentParserTests : DeserializerTestsSetup + { + private readonly TestDocumentParser _deserializer; + + public DocumentParserTests() + { + _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.GetContextEntity("test-resource").Attributes.Single(attr => attr.PublicAttributeName == member).PropertyInfo; + var deserializedValue = pi.GetValue(entity); + + if (member == "int-field") + { + Assert.Equal(deserializedValue, 1); + } + else if (member == "nullable-int-field" && value == null) + { + Assert.Equal(deserializedValue, null); + } + else if (member == "nullable-int-field" && (string)value == "1") + { + Assert.Equal(deserializedValue, 1); + } + 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.ThrowsAny(() => _deserializer.Deserialize(body)); + } + + [Fact] + public void DeserializeRelationships_PopulatedOneToOnePrincipal_NavigationPropertyIsNullAndForeignKeyIsPopulated() + { + // 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.Null(result.Principal); + 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.ThrowsAny(() => _deserializer.Deserialize(body)); + } + + [Fact] + public void DeserializeRelationships_PopulatedOneToManyPrincipal_NavigationIsNullAndForeignKeyIsPopulated() + { + // 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.Null(result.Principal); + 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.Equal(1, result.Dependents.Count); + Assert.Equal(10, result.Dependents.First().Id); + Assert.Null(result.AttributeMember); + } + } +} diff --git a/test/UnitTests/Serialization/Deserializer/ServerDeserializerTests.cs b/test/UnitTests/Serialization/Deserializer/ServerDeserializerTests.cs new file mode 100644 index 0000000000..8694064151 --- /dev/null +++ b/test/UnitTests/Serialization/Deserializer/ServerDeserializerTests.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization; +using Moq; +using Newtonsoft.Json; +using Xunit; + +namespace UnitTests.Serialization.Deserializer +{ + public class ServerDeserializerTests : DeserializerTestsSetup + { + private readonly ServerDeserializer _deserializer; + private readonly Mock _fieldsManagerMock = new Mock(); + public ServerDeserializerTests() : base() + { + _deserializer = new ServerDeserializer(_resourceGraph, _fieldsManagerMock.Object); + } + + [Fact] + public void DeserializeAttributes_VariousUpdatedMembers_RegistersUpdatedFields() + { + // 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.Relationshipss)).Returns(relationshipsToUpdate); + } + } +} 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..3728f2b4f2 --- /dev/null +++ b/test/UnitTests/Serialization/SerializationTestsSetupBase.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using Bogus; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization; + +namespace UnitTests.Serialization +{ + public class SerializationTestsSetupBase + { + protected IResourceGraph _resourceGraph; + protected IContextEntityProvider _provider; + protected readonly Faker _foodFaker; + protected readonly Faker _songFaker; + protected readonly Faker
_articleFaker; + protected readonly Faker _blogFaker; + protected readonly Faker _personFaker; + protected readonly JsonApiSerializerSettings _defaultSettings = new JsonApiSerializerSettings(); + + 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(); + } + + 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; } + } + + 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; } + } + } +} \ No newline at end of file diff --git a/test/UnitTests/Serialization/Serializer/ClientSerializerTests.cs b/test/UnitTests/Serialization/Serializer/ClientSerializerTests.cs new file mode 100644 index 0000000000..c457cd7723 --- /dev/null +++ b/test/UnitTests/Serialization/Serializer/ClientSerializerTests.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Models; +using Xunit; + +namespace UnitTests.Serialization.Serializer +{ + public class ClientSerializerTests : SerializerTestsSetup + { + private readonly ClientSerializer _serializer; + + public ClientSerializerTests() + { + _serializer = new ClientSerializer(_fieldExplorer, _resourceGraph, _resourceGraph); + } + + + [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); + Console.WriteLine(serialized); + // 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/Serializer/DocumentBuilderTests.cs b/test/UnitTests/Serialization/Serializer/DocumentBuilderTests.cs new file mode 100644 index 0000000000..a2095cc82a --- /dev/null +++ b/test/UnitTests/Serialization/Serializer/DocumentBuilderTests.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Models; +using Xunit; + +namespace UnitTests.Serialization.Serializer +{ + public class DocumentBuilderTests : SerializerTestsSetup + { + private readonly TestSerializer _serializer; + + public DocumentBuilderTests() + { + _serializer = new TestSerializer(_resourceGraph, _resourceGraph); + } + + [Fact] + public void ResourceToDocument_EmptyResource_CanBuild() + { + // arrange + var entity = new TestResource(); + + // act + var document = _serializer.Build(entity); + var data = (ResourceObject)document.Data; + + // assert + Assert.Null(data.Attributes); + Assert.Null(data.Relationships); + Assert.Null(data.Id); + Assert.Equal("test-resource", data.Type); + } + + [Fact] + public void ResourceToDocument_ResourceWithId_CanBuild() + { + // arrange + var entity = new TestResource() { Id = 1 }; + + // act + var document = _serializer.Build(entity); + var data = (ResourceObject)document.Data; + + // assert + Assert.Equal("1", data.Id); + Assert.Null(data.Attributes); + Assert.Null(data.Relationships); + Assert.Equal("test-resource", data.Type); + } + + [Theory] + [InlineData(null, null)] + [InlineData("string field", 1)] + public void ResourceToDocument_ResourceWithIncludedAttrs_CanBuild(string stringFieldValue, int? intFieldValue) + { + // arrange + var entity = new TestResource() { StringField = stringFieldValue, NullableIntField = intFieldValue }; + var attrs = _fieldExplorer.GetAttributes(tr => new { tr.StringField, tr.NullableIntField }); + // act + var document = _serializer.Build(entity, attrs); + var data = (ResourceObject)document.Data; + + // assert + Assert.NotNull(data.Attributes); + Assert.Equal(2, data.Attributes.Keys.Count); + Assert.Equal(stringFieldValue, data.Attributes["string-field"]); + Assert.Equal(intFieldValue, data.Attributes["nullable-int-field"]); + } + + + + [Theory] + [InlineData(null, null)] + [InlineData("string field", 1)] + public void ResourceListToDocument_ResourcesWithIncludedAttrs_CanBuild(string stringFieldValue, int? intFieldValue) + { + // arrange + var entities = new List() + { + new TestResource() { Id = 1, StringField = stringFieldValue, NullableIntField = intFieldValue }, + new TestResource() { Id = 2, StringField = stringFieldValue, NullableIntField = intFieldValue } + }; + var attrs = _fieldExplorer.GetAttributes(tr => new { tr.StringField, tr.NullableIntField }); + // act + var document = _serializer.Build(entities, attrs); + var data = (List)document.Data; + + // assert + + Assert.Equal(2, data.Count); + foreach (var ro in data) + { + Assert.Equal(2, ro.Attributes.Keys.Count); + Assert.Equal(stringFieldValue, ro.Attributes["string-field"]); + Assert.Equal(intFieldValue, ro.Attributes["nullable-int-field"]); + } + } + + [Fact] + public void ResourceWithRelationshipsToDocument_EmptyResource_CanBuild() + { + // arrange + var entity = new MultipleRelationshipsPrincipalPart(); + + // act + var document = _serializer.Build(entity); + var data = (ResourceObject)document.Data; + + // assert + Assert.Null(data.Attributes); + Assert.Null(data.Relationships); + Assert.Null(data.Id); + Assert.Equal("multi-principals", data.Type); + } + + [Fact] + public void ResourceWithRelationshipsToDocument_ResourceWithId_CanBuild() + { + // arrange + var entity = new MultipleRelationshipsPrincipalPart + { + PopulatedToOne = new OneToOneDependent { Id = 10 }, + }; + + // act + var document = _serializer.Build(entity); + var data = (ResourceObject)document.Data; + + // assert + Assert.Null(data.Attributes); + Assert.Null(data.Relationships); + Assert.Null(data.Id); + Assert.Equal("multi-principals", data.Type); + } + + [Fact] + public void ResourceWithRelationshipsToDocument_WithIncludedRelationshipsAttributes_CanBuild() + { + // arrange + var entity = new MultipleRelationshipsPrincipalPart + { + PopulatedToOne = new OneToOneDependent { Id = 10 }, + PopulatedToManies = new List { new OneToManyDependent { Id = 20 } } + }; + var relationships = _fieldExplorer.GetRelationships(tr => new { tr.PopulatedToManies, tr.PopulatedToOne, tr.EmptyToOne, tr.EmptyToManies }); + + // act + var document = _serializer.Build(entity, relationships: relationships); + var data = (ResourceObject)document.Data; + + // assert + Assert.Equal(4, data.Relationships.Count); + Assert.Null(data.Relationships["empty-to-one"].Data); + Assert.Empty((IList)data.Relationships["empty-to-manies"].Data); + var populatedToOneData = (ResourceIdentifierObject)data.Relationships["populated-to-one"].Data; + Assert.NotNull(populatedToOneData); + Assert.Equal("10", populatedToOneData.Id); + Assert.Equal("one-to-one-dependents", populatedToOneData.Type); + var populatedToManiesData = (List)data.Relationships["populated-to-manies"].Data; + Assert.Equal(1, populatedToManiesData.Count); + Assert.Equal("20", populatedToManiesData.First().Id); + Assert.Equal("one-to-many-dependents", populatedToManiesData.First().Type); + } + + [Fact] + public void ResourceWithRelationshipsToDocument_DeviatingForeignKeyWhileRelationshipIncluded_IgnoresForeignKeyDuringBuild() + { + // arrange + var entity = new OneToOneDependent { Principal = new OneToOnePrincipal { Id = 10 }, PrincipalId = 123 }; + var relationships = _fieldExplorer.GetRelationships(tr => tr.Principal); + + // act + var document = _serializer.Build(entity, relationships: relationships); + var data = (ResourceObject)document.Data; + + // assert + Assert.Equal(1, data.Relationships.Count); + Assert.NotNull(data.Relationships["principal"].Data); + var ro = (ResourceIdentifierObject)data.Relationships["principal"].Data; + Assert.Equal("10", ro.Id); + } + + [Fact] + public void ResourceWithRelationshipsToDocument_DeviatingForeignKeyAndNoNavigationWhileRelationshipIncluded_IgnoresForeignKeyDuringBuild() + { + // arrange + var entity = new OneToOneDependent { Principal = null, PrincipalId = 123 }; + var relationships = _fieldExplorer.GetRelationships(tr => tr.Principal); + + // act + var document = _serializer.Build(entity, relationships: relationships); + var data = (ResourceObject)document.Data; + + // assert + Assert.Null(data.Relationships["principal"].Data); + } + + [Fact] + public void ResourceWithRequiredRelationshipsToDocument_DeviatingForeignKeyWhileRelationshipIncluded_IgnoresForeignKeyDuringBuild() + { + // arrange + var entity = new OneToOneRequiredDependent { Principal = new OneToOnePrincipal { Id = 10 }, PrincipalId = 123 }; + var relationships = _fieldExplorer.GetRelationships(tr => tr.Principal); + + // act + var document = _serializer.Build(entity, relationships: relationships); + var data = (ResourceObject)document.Data; + + // assert + Assert.Equal(1, data.Relationships.Count); + Assert.NotNull(data.Relationships["principal"].Data); + var ro = (ResourceIdentifierObject)data.Relationships["principal"].Data; + Assert.Equal("10", ro.Id); + } + + [Fact] + public void ResourceWithRequiredRelationshipsToDocument_DeviatingForeignKeyAndNoNavigationWhileRelationshipIncluded_ThrowsNotSupportedException() + { + // arrange + var entity = new OneToOneRequiredDependent { Principal = null, PrincipalId = 123 }; + var relationships = _fieldExplorer.GetRelationships(tr => tr.Principal); + + // act & assert + Assert.ThrowsAny(() => _serializer.Build(entity, relationships: relationships)); + } + + [Fact] + public void ResourceWithRequiredRelationshipsToDocument_EmptyResourceWhileRelationshipIncluded_ThrowsNotSupportedException() + { + // arrange + var entity = new OneToOneRequiredDependent(); + var relationships = _fieldExplorer.GetRelationships(tr => tr.Principal); + + // act & assert + Assert.ThrowsAny(() => _serializer.Build(entity, relationships: relationships)); + } + } +} diff --git a/test/UnitTests/Serialization/Serializer/IncludedRelationshipsBuilderTests.cs b/test/UnitTests/Serialization/Serializer/IncludedRelationshipsBuilderTests.cs new file mode 100644 index 0000000000..c3b4e439f8 --- /dev/null +++ b/test/UnitTests/Serialization/Serializer/IncludedRelationshipsBuilderTests.cs @@ -0,0 +1,178 @@ +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Models; +using Xunit; +using UnitTests.Serialization.Serializer; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Internal.Query; + +namespace UnitTests.Serialization.IncludedRelationshipBuilder +{ + public class IncludedRelationshipBuilderTests : 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.Equal(1, blog.Relationships.Keys.ToList().Count); + + 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.Equal(1, result.Count); + 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.GetContextEntity
(); + 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.GetContextEntity(relationship.DependentType); + } + return parsedChain; + } + + private IncludedRelationshipsBuilder GetBuilder() + { + var fields = GetSerializableFields(); + var links = GetLinkBuilder(); + return new IncludedRelationshipsBuilder(fields, links, _resourceGraph, _resourceGraph); + } + + } +} diff --git a/test/UnitTests/Serialization/Serializer/SerializerTestsSetup.cs b/test/UnitTests/Serialization/Serializer/SerializerTestsSetup.cs new file mode 100644 index 0000000000..6c95a37ecf --- /dev/null +++ b/test/UnitTests/Serialization/Serializer/SerializerTestsSetup.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.Links; +using JsonApiDotNetCore.Services; +using Moq; + +namespace UnitTests.Serialization.Serializer +{ + public class SerializerTestsSetup : SerializationTestsSetupBase + { + protected readonly IExposedFieldExplorer _fieldExplorer; + protected readonly TopLevelLinks _dummyToplevelLinks; + protected readonly ResourceLinks _dummyResourceLinks; + protected readonly RelationshipLinks _dummyRelationshipLinks; + public SerializerTestsSetup() + { + _fieldExplorer = new ExposedFieldExplorer(_resourceGraph); + _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 ServerSerializer GetServerSerializer(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 serializableFields = GetSerializableFields(); + var sparseFields = GetFieldsQuery(); + var included = GetIncludedRelationships(inclusionChains); + var provider = GetContextEntityProvider(); + var includedBuilder = GetIncludedBuilder(); + + return new ServerSerializer(meta, link, includedBuilder, serializableFields, included, sparseFields, _resourceGraph, provider); + } + + private IIncludedRelationshipsBuilder GetIncludedBuilder() + { + return new IncludedRelationshipsBuilder(GetSerializableFields(), GetLinkBuilder(), _resourceGraph, _resourceGraph); + } + + private IContextEntityProvider GetContextEntityProvider() + { + 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 IRequestContext GetRequestManager() where T : class, IIdentifiable + { + var mock = new Mock(); + mock.Setup(m => m.GetRequestResource()).Returns(_resourceGraph.GetContextEntity()); + return mock.Object; + } + + protected ILinkBuilder GetLinkBuilder(TopLevelLinks top = null, ResourceLinks resource = null, RelationshipLinks relationship = null) + { + var mock = new Mock(); + mock.Setup(m => m.GetTopLevelLinks()).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 IFieldsQueryService GetFieldsQuery() + { + var mock = new Mock(); + return mock.Object; + } + + protected ISerializableFields GetSerializableFields() + { + var mock = new Mock(); + mock.Setup(m => m.GetAllowedAttributes(It.IsAny())).Returns(t => _resourceGraph.GetContextEntity(t).Attributes); + mock.Setup(m => m.GetAllowedRelationships(It.IsAny())).Returns(t => _resourceGraph.GetContextEntity(t).Relationships); + return mock.Object; + } + + protected IIncludedQueryService 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 TestSerializer : DocumentBuilder + { + public TestSerializer(IResourceGraph resourceGraph, IContextEntityProvider provider) : base(resourceGraph, provider) { } + + public new Document Build(IIdentifiable entity, List attributes = null, List relationships = null) + { + return base.Build(entity, attributes, relationships); + } + + public new Document Build(IEnumerable entities, List attributes = null, List relationships = null) + { + return base.Build(entities, attributes, relationships); + } + } + } +} \ No newline at end of file diff --git a/test/UnitTests/Serialization/Serializer/ServerSerializerTests.cs b/test/UnitTests/Serialization/Serializer/ServerSerializerTests.cs new file mode 100644 index 0000000000..ac397086b1 --- /dev/null +++ b/test/UnitTests/Serialization/Serializer/ServerSerializerTests.cs @@ -0,0 +1,411 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.Links; +using Xunit; + +namespace UnitTests.Serialization.Serializer +{ + public class ServerSerializerTests : SerializerTestsSetup + { + + [Fact] + public void SerializeSingle_ResourceWithDefaultTargetFields_CanBuild() + { + // arrange + var entity = new TestResource() { Id = 1, StringField = "value", NullableIntField = 123 }; + var serializer = GetServerSerializer(); + + // 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_CanBuild() + { + // arrange + var entity = new TestResource() { Id = 1, StringField = "value", NullableIntField = 123 }; + var serializer = GetServerSerializer(); + + // 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 = _fieldExplorer.GetRelationships().Select(r => new List { r }).ToList(); + var serializer = GetServerSerializer(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 = _fieldExplorer.GetRelationships() + .Select(r => + { + var chain = new List { r }; + if (r.PublicRelationshipName != "populated-to-manies") + return new List { r }; + chain.AddRange(_fieldExplorer.GetRelationships()); + return chain; + }).ToList(); + + var serializer = GetServerSerializer(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 = GetServerSerializer(); + 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 = GetServerSerializer(); + // 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 includeRelationshipsOn = new List { typeof(OneToManyPrincipal) }; + + var serializer = GetServerSerializer(topLinks: _dummyToplevelLinks, relationshipLinks: _dummyRelationshipLinks, resourceLinks: _dummyResourceLinks); + // act + string serialized = serializer.SerializeSingle(entity); + + Console.WriteLine(serialized); + // 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_ResourceNoLinksNoRelationships_DoesNotSerializeRelationshipMember() + { + // arrange + var entity = new OneToManyPrincipal { Id = 10 }; + + var serializer = GetServerSerializer(); + // act + string serialized = serializer.SerializeSingle(entity); + + Console.WriteLine(serialized); + // assert + var expectedFormatted = + @"{ + ""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_ResourceWithMeta_IncludesMetaInResult() + { + // arrange + var meta = new Dictionary { { "test", "meta" } }; + var entity = new OneToManyPrincipal { Id = 10 }; + var serializer = GetServerSerializer(metaDict: meta); + // act + string serialized = serializer.SerializeSingle(entity); + + Console.WriteLine(serialized); + // 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 = GetServerSerializer(metaDict: meta, topLinks: _dummyToplevelLinks, relationshipLinks: _dummyRelationshipLinks, resourceLinks: _dummyResourceLinks); + // act + string serialized = serializer.SerializeSingle(entity); + + Console.WriteLine(serialized); + // 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); + } + } +} diff --git a/test/UnitTests/Services/EntityResourceService_Tests.cs b/test/UnitTests/Services/EntityResourceService_Tests.cs index 0643110b21..f7a3b59dfd 100644 --- a/test/UnitTests/Services/EntityResourceService_Tests.cs +++ b/test/UnitTests/Services/EntityResourceService_Tests.cs @@ -72,7 +72,9 @@ public async Task GetRelationshipAsync_Returns_Relationship_Value() Assert.Equal(todoItem.Collection.Id, collection.Id); } - private EntityResourceService GetService() => - new EntityResourceService(_jsonApiContextMock.Object, _repositoryMock.Object, _loggerFactory, null); + private EntityResourceService GetService() + { + return new EntityResourceService(_repositoryMock.Object,_jsonApiContextMock.Object.Options, _jsonApiContextMock.Object.RequestManager, _jsonApiContextMock.Object.PageManager, _jsonApiContextMock.Object.ResourceGraph, _loggerFactory, null); + } } } 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 index aa8bc6ae7e..9a8c98f69b 100644 --- a/test/UnitTests/Services/QueryAccessorTests.cs +++ b/test/UnitTests/Services/QueryAccessorTests.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Collections.Generic; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -12,21 +13,21 @@ namespace UnitTests.Services { public class QueryAccessorTests { - private readonly Mock _contextMock; + private readonly Mock _rmMock; private readonly Mock> _loggerMock; private readonly Mock _queryMock; public QueryAccessorTests() { - _contextMock = new Mock(); + _rmMock = new Mock(); _loggerMock = new Mock>(); _queryMock = new Mock(); } [Fact] - public void Can_Get_Guid_QueryValue() + public void GetGuid_GuidIsValid_IsReturnedCorrectly() { - // arrange + // Arrange const string key = "SomeId"; var value = Guid.NewGuid(); var querySet = new QuerySet @@ -36,9 +37,9 @@ public void Can_Get_Guid_QueryValue() } }; - _contextMock.Setup(c => c.QuerySet).Returns(querySet); + _rmMock.Setup(c => c.QuerySet).Returns(querySet); - var service = new QueryAccessor(_contextMock.Object, _loggerMock.Object); + var service = new QueryAccessor(_rmMock.Object, _loggerMock.Object); // act var success = service.TryGetValue("SomeId", out Guid result); @@ -51,7 +52,7 @@ public void Can_Get_Guid_QueryValue() [Fact] public void GetRequired_Throws_If_Not_Present() { - // arrange + // Arrange const string key = "SomeId"; var value = Guid.NewGuid(); @@ -62,9 +63,9 @@ public void GetRequired_Throws_If_Not_Present() } }; - _contextMock.Setup(c => c.QuerySet).Returns(querySet); + _rmMock.Setup(c => c.QuerySet).Returns(querySet); - var service = new QueryAccessor(_contextMock.Object, _loggerMock.Object); + var service = new QueryAccessor(_rmMock.Object, _loggerMock.Object); // act var exception = Assert.Throws(() => service.GetRequired("Invalid")); @@ -87,14 +88,14 @@ public void GetRequired_Does_Not_Throw_If_Present() } }; - _contextMock.Setup(c => c.QuerySet).Returns(querySet); + _rmMock.Setup(c => c.QuerySet).Returns(querySet); - var service = new QueryAccessor(_contextMock.Object, _loggerMock.Object); + var service = new QueryAccessor(_rmMock.Object, _loggerMock.Object); - // act + // Act var result = service.GetRequired("SomeId"); - // assert + // Assert Assert.Equal(value, result); } } diff --git a/test/UnitTests/Services/QueryComposerTests.cs b/test/UnitTests/Services/QueryComposerTests.cs index efa600f2f3..4f31a7f4c6 100644 --- a/test/UnitTests/Services/QueryComposerTests.cs +++ b/test/UnitTests/Services/QueryComposerTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Services; using Moq; using Xunit; @@ -25,13 +26,14 @@ public void Can_ComposeEqual_FilterStringForUrl() filters.Add(filter); querySet.Filters = filters; - _jsonApiContext + var rmMock = new Mock(); + rmMock .Setup(m => m.QuerySet) .Returns(querySet); var queryComposer = new QueryComposer(); // act - var filterString = queryComposer.Compose(_jsonApiContext.Object); + var filterString = queryComposer.Compose(rmMock.Object); // assert Assert.Equal("&filter[attribute]=eq:value", filterString); } @@ -47,14 +49,15 @@ public void Can_ComposeLessThan_FilterStringForUrl() filters.Add(filter); filters.Add(filter2); querySet.Filters = filters; - - _jsonApiContext + var rmMock = new Mock(); + rmMock .Setup(m => m.QuerySet) .Returns(querySet); + var queryComposer = new QueryComposer(); // act - var filterString = queryComposer.Compose(_jsonApiContext.Object); + var filterString = queryComposer.Compose(rmMock.Object); // assert Assert.Equal("&filter[attribute]=le:value&filter[attribute2]=value2", filterString); } @@ -65,13 +68,15 @@ public void NoFilter_Compose_EmptyStringReturned() // arrange var querySet = new QuerySet(); - _jsonApiContext + var rmMock = new Mock(); + rmMock .Setup(m => m.QuerySet) .Returns(querySet); var queryComposer = new QueryComposer(); - // act - var filterString = queryComposer.Compose(_jsonApiContext.Object); + // Act + + var filterString = queryComposer.Compose(rmMock.Object); // assert Assert.Equal("", filterString); } diff --git a/test/UnitTests/Services/QueryParserTests.cs b/test/UnitTests/Services/QueryParserTests.cs index 58cd6251e9..ac0200f8e2 100644 --- a/test/UnitTests/Services/QueryParserTests.cs +++ b/test/UnitTests/Services/QueryParserTests.cs @@ -3,6 +3,8 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; @@ -14,12 +16,15 @@ namespace UnitTests.Services { public class QueryParserTests { - private readonly Mock _controllerContextMock; + private readonly Mock _requestMock; private readonly Mock _queryCollectionMock; + private readonly IInternalFieldsQueryService _fieldsQuery = new Mock().Object; + private readonly IInternalIncludedQueryService _includedQuery = new Mock().Object; + private readonly IContextEntityProvider _graph = new Mock().Object; public QueryParserTests() { - _controllerContextMock = new Mock(); + _requestMock = new Mock(); _queryCollectionMock = new Mock(); } @@ -35,11 +40,11 @@ public void Can_Build_Filters() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.None)); + _requestMock + .Setup(m => m.DisabledQueryParams) + .Returns(QueryParams.None); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_includedQuery, _fieldsQuery, _requestMock.Object, _graph, new JsonApiOptions()); // act var querySet = queryParser.Parse(_queryCollectionMock.Object); @@ -61,11 +66,11 @@ public void Filters_Properly_Parses_DateTime_With_Operation() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.None)); + _requestMock + .Setup(m => m.DisabledQueryParams) + .Returns(QueryParams.None); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_includedQuery, _fieldsQuery, _requestMock.Object, _graph, new JsonApiOptions()); // act var querySet = queryParser.Parse(_queryCollectionMock.Object); @@ -88,11 +93,11 @@ public void Filters_Properly_Parses_DateTime_Without_Operation() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.None)); + _requestMock + .Setup(m => m.DisabledQueryParams) + .Returns(QueryParams.None); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_includedQuery, _fieldsQuery, _requestMock.Object, _graph, new JsonApiOptions()); // act var querySet = queryParser.Parse(_queryCollectionMock.Object); @@ -105,7 +110,7 @@ public void Filters_Properly_Parses_DateTime_Without_Operation() [Fact] public void Can_Disable_Filters() { - // arrange + // Arrange var query = new Dictionary { { "filter[key]", new StringValues("value") } }; @@ -114,16 +119,16 @@ public void Can_Disable_Filters() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.Filter)); + _requestMock + .Setup(m => m.DisabledQueryParams) + .Returns(QueryParams.Filters); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_includedQuery, _fieldsQuery, _requestMock.Object, _graph, new JsonApiOptions()); - // act + // Act var querySet = queryParser.Parse(_queryCollectionMock.Object); - // assert + // Assert Assert.Empty(querySet.Filters); } [Theory] @@ -141,7 +146,7 @@ public void Parse_EmptySortSegment_ReceivesJsonApiException(string stringSortQue .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_includedQuery, _fieldsQuery, _requestMock.Object, _graph, new JsonApiOptions()); // Act / Assert var exception = Assert.Throws(() => @@ -162,11 +167,11 @@ public void Can_Disable_Sort() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.Sort)); + _requestMock + .Setup(m => m.DisabledQueryParams) + .Returns(QueryParams.Sort); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_includedQuery, _fieldsQuery, _requestMock.Object, _graph, new JsonApiOptions()); // act var querySet = queryParser.Parse(_queryCollectionMock.Object); @@ -187,11 +192,11 @@ public void Can_Disable_Include() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.Include)); + _requestMock + .Setup(m => m.DisabledQueryParams) + .Returns(QueryParams.Include); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_includedQuery, _fieldsQuery, _requestMock.Object, _graph, new JsonApiOptions()); // act var querySet = queryParser.Parse(_queryCollectionMock.Object); @@ -212,17 +217,17 @@ public void Can_Disable_Page() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.Page)); + _requestMock + .Setup(m => m.DisabledQueryParams) + .Returns(QueryParams.Page); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_includedQuery, _fieldsQuery, _requestMock.Object, _graph, new JsonApiOptions()); // act var querySet = queryParser.Parse(_queryCollectionMock.Object); // assert - Assert.Equal(0, querySet.PageQuery.PageSize); + Assert.Equal(null, querySet.PageQuery.PageSize); } [Fact] @@ -237,16 +242,16 @@ public void Can_Disable_Fields() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.Fields)); + _requestMock + .Setup(m => m.DisabledQueryParams) + .Returns(QueryParams.Fields); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_includedQuery, _fieldsQuery, _requestMock.Object, _graph, new JsonApiOptions()); // act var querySet = queryParser.Parse(_queryCollectionMock.Object); - // assert + // Assert Assert.Empty(querySet.Fields); } @@ -264,8 +269,8 @@ public void Can_Parse_Fields_Query() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.RequestEntity) + _requestMock + .Setup(m => m.GetRequestResource()) .Returns(new ContextEntity { EntityName = type, @@ -279,7 +284,7 @@ public void Can_Parse_Fields_Query() Relationships = new List() }); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_includedQuery, _fieldsQuery, _requestMock.Object, _graph, new JsonApiOptions()); // act var querySet = queryParser.Parse(_queryCollectionMock.Object); @@ -304,8 +309,8 @@ public void Throws_JsonApiException_If_Field_DoesNotExist() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.RequestEntity) + _requestMock + .Setup(m => m.GetRequestResource()) .Returns(new ContextEntity { EntityName = type, @@ -313,7 +318,7 @@ public void Throws_JsonApiException_If_Field_DoesNotExist() Relationships = new List() }); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_includedQuery, _fieldsQuery, _requestMock.Object, _graph, new JsonApiOptions()); // act , assert var ex = Assert.Throws(() => queryParser.Parse(_queryCollectionMock.Object)); @@ -335,7 +340,7 @@ public void Can_Parse_Page_Size_Query(string value, int expectedValue, bool shou .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_includedQuery, _fieldsQuery, _requestMock.Object, _graph, new JsonApiOptions()); // act if (shouldThrow) @@ -365,7 +370,7 @@ public void Can_Parse_Page_Number_Query(string value, int expectedValue, bool sh .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_includedQuery, _fieldsQuery, _requestMock.Object, _graph, new JsonApiOptions()); // act if (shouldThrow) diff --git a/test/UnitTests/UnitTests.csproj b/test/UnitTests/UnitTests.csproj index b4f96631ca..924aa5a434 100644 --- a/test/UnitTests/UnitTests.csproj +++ b/test/UnitTests/UnitTests.csproj @@ -8,12 +8,13 @@ - - + + + - + @@ -22,9 +23,7 @@ - - - - + +