diff --git a/.gitignore b/.gitignore index a2dffca805..cd9f35a530 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ *.user .couscous/ docs/Template-Dark/ +.idea/ \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 4ae919b700..725d8335be 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -6,7 +6,7 @@ "isShellCommand": true, "args": [], "options": { - "cwd": "${workspaceRoot}/src/JsonApiDotNetCoreExample" + "cwd": "${workspaceRoot}/src/Examples/JsonApiDotNetCoreExample" }, "tasks": [ { diff --git a/JsonApiDotnetCore.sln b/JsonApiDotnetCore.sln index e3c197bbc5..b4ff89b0aa 100644 --- a/JsonApiDotnetCore.sln +++ b/JsonApiDotnetCore.sln @@ -4,7 +4,7 @@ VisualStudioVersion = 15.0.26228.9 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\JsonApiDotNetCoreExample\JsonApiDotNetCoreExample.csproj", "{97EE048B-16C0-43F6-BDA9-4E762B2F579F}" +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 @@ -18,12 +18,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoEntityFrameworkExample", "src\NoEntityFrameworkExample\NoEntityFrameworkExample.csproj", "{570165EC-62B5-4684-A139-8D2A30DD4475}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoEntityFrameworkExample", "src\Examples\NoEntityFrameworkExample\NoEntityFrameworkExample.csproj", "{570165EC-62B5-4684-A139-8D2A30DD4475}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoEntityFrameworkTests", "test\NoEntityFrameworkTests\NoEntityFrameworkTests.csproj", "{73DA578D-A63F-4956-83ED-6D7102E09140}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "test\UnitTests\UnitTests.csproj", "{6D4BD85A-A262-44C6-8572-FE3A30410BF3}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{026FBC6C-AF76-4568-9B87-EC73457899FD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReportsExample", "src\Examples\ReportsExample\ReportsExample.csproj", "{FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -94,6 +98,18 @@ Global {6D4BD85A-A262-44C6-8572-FE3A30410BF3}.Release|x64.Build.0 = Release|x64 {6D4BD85A-A262-44C6-8572-FE3A30410BF3}.Release|x86.ActiveCfg = Release|x86 {6D4BD85A-A262-44C6-8572-FE3A30410BF3}.Release|x86.Build.0 = Release|x86 + {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|x64 + {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Debug|x64.Build.0 = Debug|x64 + {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Debug|x86.ActiveCfg = Debug|x86 + {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Debug|x86.Build.0 = Debug|x86 + {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|x64 + {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|x64.Build.0 = Release|x64 + {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|x86.ActiveCfg = Release|x86 + {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -105,5 +121,7 @@ Global {570165EC-62B5-4684-A139-8D2A30DD4475} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF} {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} EndGlobalSection EndGlobal diff --git a/src/JsonApiDotNetCoreExample/.gitignore b/src/Examples/JsonApiDotNetCoreExample/.gitignore similarity index 100% rename from src/JsonApiDotNetCoreExample/.gitignore rename to src/Examples/JsonApiDotNetCoreExample/.gitignore diff --git a/src/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs rename to src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs diff --git a/src/JsonApiDotNetCoreExample/Controllers/PeopleController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Controllers/PeopleController.cs rename to src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs diff --git a/src/JsonApiDotNetCoreExample/Controllers/Restricted/ReadOnlyController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/Restricted/ReadOnlyController.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Controllers/Restricted/ReadOnlyController.cs rename to src/Examples/JsonApiDotNetCoreExample/Controllers/Restricted/ReadOnlyController.cs diff --git a/src/JsonApiDotNetCoreExample/Controllers/TestValuesController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TestValuesController.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Controllers/TestValuesController.cs rename to src/Examples/JsonApiDotNetCoreExample/Controllers/TestValuesController.cs diff --git a/src/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs rename to src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs diff --git a/src/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs rename to src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs diff --git a/src/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs similarity index 97% rename from src/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs rename to src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs index 977cb4f806..de784c129a 100644 --- a/src/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs @@ -54,7 +54,7 @@ public CustomJsonApiController( IResourceService resourceService, ILoggerFactory loggerFactory) { - _jsonApiContext = jsonApiContext.ApplyContext(); + _jsonApiContext = jsonApiContext.ApplyContext(this); _resourceService = resourceService; _logger = loggerFactory.CreateLogger>(); } @@ -63,7 +63,7 @@ public CustomJsonApiController( IJsonApiContext jsonApiContext, IResourceService resourceService) { - _jsonApiContext = jsonApiContext.ApplyContext(); + _jsonApiContext = jsonApiContext.ApplyContext(this); _resourceService = resourceService; } diff --git a/src/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs rename to src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs diff --git a/src/JsonApiDotNetCoreExample/Data/AppDbContext.cs b/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Data/AppDbContext.cs rename to src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs diff --git a/src/JsonApiDotNetCoreExample/Dockerfile b/src/Examples/JsonApiDotNetCoreExample/Dockerfile similarity index 100% rename from src/JsonApiDotNetCoreExample/Dockerfile rename to src/Examples/JsonApiDotNetCoreExample/Dockerfile diff --git a/src/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj b/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj similarity index 95% rename from src/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj rename to src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj index 112ecce229..03ac91d4ae 100755 --- a/src/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj +++ b/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/JsonApiDotNetCoreExample/Migrations/20170315140127_initial.Designer.cs b/src/Examples/JsonApiDotNetCoreExample/Migrations/20170315140127_initial.Designer.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Migrations/20170315140127_initial.Designer.cs rename to src/Examples/JsonApiDotNetCoreExample/Migrations/20170315140127_initial.Designer.cs diff --git a/src/JsonApiDotNetCoreExample/Migrations/20170315140127_initial.cs b/src/Examples/JsonApiDotNetCoreExample/Migrations/20170315140127_initial.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Migrations/20170315140127_initial.cs rename to src/Examples/JsonApiDotNetCoreExample/Migrations/20170315140127_initial.cs diff --git a/src/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.Designer.cs b/src/Examples/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.Designer.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.Designer.cs rename to src/Examples/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.Designer.cs diff --git a/src/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.cs b/src/Examples/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.cs rename to src/Examples/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.cs diff --git a/src/JsonApiDotNetCoreExample/Migrations/20170330234539_AddGuidProperty.Designer.cs b/src/Examples/JsonApiDotNetCoreExample/Migrations/20170330234539_AddGuidProperty.Designer.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Migrations/20170330234539_AddGuidProperty.Designer.cs rename to src/Examples/JsonApiDotNetCoreExample/Migrations/20170330234539_AddGuidProperty.Designer.cs diff --git a/src/JsonApiDotNetCoreExample/Migrations/20170330234539_AddGuidProperty.cs b/src/Examples/JsonApiDotNetCoreExample/Migrations/20170330234539_AddGuidProperty.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Migrations/20170330234539_AddGuidProperty.cs rename to src/Examples/JsonApiDotNetCoreExample/Migrations/20170330234539_AddGuidProperty.cs diff --git a/src/JsonApiDotNetCoreExample/Migrations/20170424180950_AddCreatesAndAchievedDates.Designer.cs b/src/Examples/JsonApiDotNetCoreExample/Migrations/20170424180950_AddCreatesAndAchievedDates.Designer.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Migrations/20170424180950_AddCreatesAndAchievedDates.Designer.cs rename to src/Examples/JsonApiDotNetCoreExample/Migrations/20170424180950_AddCreatesAndAchievedDates.Designer.cs diff --git a/src/JsonApiDotNetCoreExample/Migrations/20170424180950_AddCreatesAndAchievedDates.cs b/src/Examples/JsonApiDotNetCoreExample/Migrations/20170424180950_AddCreatesAndAchievedDates.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Migrations/20170424180950_AddCreatesAndAchievedDates.cs rename to src/Examples/JsonApiDotNetCoreExample/Migrations/20170424180950_AddCreatesAndAchievedDates.cs diff --git a/src/JsonApiDotNetCoreExample/Migrations/20170426232509_AddCamelCasedModel.Designer.cs b/src/Examples/JsonApiDotNetCoreExample/Migrations/20170426232509_AddCamelCasedModel.Designer.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Migrations/20170426232509_AddCamelCasedModel.Designer.cs rename to src/Examples/JsonApiDotNetCoreExample/Migrations/20170426232509_AddCamelCasedModel.Designer.cs diff --git a/src/JsonApiDotNetCoreExample/Migrations/20170426232509_AddCamelCasedModel.cs b/src/Examples/JsonApiDotNetCoreExample/Migrations/20170426232509_AddCamelCasedModel.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Migrations/20170426232509_AddCamelCasedModel.cs rename to src/Examples/JsonApiDotNetCoreExample/Migrations/20170426232509_AddCamelCasedModel.cs diff --git a/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs b/src/Examples/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs rename to src/Examples/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs diff --git a/src/JsonApiDotNetCoreExample/Models/CamelCasedModel.cs b/src/Examples/JsonApiDotNetCoreExample/Models/CamelCasedModel.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Models/CamelCasedModel.cs rename to src/Examples/JsonApiDotNetCoreExample/Models/CamelCasedModel.cs diff --git a/src/JsonApiDotNetCoreExample/Models/Person.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Models/Person.cs rename to src/Examples/JsonApiDotNetCoreExample/Models/Person.cs diff --git a/src/JsonApiDotNetCoreExample/Models/TodoItem.cs b/src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Models/TodoItem.cs rename to src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs diff --git a/src/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs b/src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs rename to src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs diff --git a/src/JsonApiDotNetCoreExample/Program.cs b/src/Examples/JsonApiDotNetCoreExample/Program.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Program.cs rename to src/Examples/JsonApiDotNetCoreExample/Program.cs diff --git a/src/JsonApiDotNetCoreExample/Properties/launchSettings.json b/src/Examples/JsonApiDotNetCoreExample/Properties/launchSettings.json similarity index 100% rename from src/JsonApiDotNetCoreExample/Properties/launchSettings.json rename to src/Examples/JsonApiDotNetCoreExample/Properties/launchSettings.json diff --git a/src/JsonApiDotNetCoreExample/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startup.cs similarity index 100% rename from src/JsonApiDotNetCoreExample/Startup.cs rename to src/Examples/JsonApiDotNetCoreExample/Startup.cs diff --git a/src/JsonApiDotNetCoreExample/appsettings.json b/src/Examples/JsonApiDotNetCoreExample/appsettings.json similarity index 100% rename from src/JsonApiDotNetCoreExample/appsettings.json rename to src/Examples/JsonApiDotNetCoreExample/appsettings.json diff --git a/src/JsonApiDotNetCoreExample/web.config b/src/Examples/JsonApiDotNetCoreExample/web.config similarity index 100% rename from src/JsonApiDotNetCoreExample/web.config rename to src/Examples/JsonApiDotNetCoreExample/web.config diff --git a/src/NoEntityFrameworkExample/.gitignore b/src/Examples/NoEntityFrameworkExample/.gitignore similarity index 100% rename from src/NoEntityFrameworkExample/.gitignore rename to src/Examples/NoEntityFrameworkExample/.gitignore diff --git a/src/NoEntityFrameworkExample/Controllers/CustomTodoItemsController.cs b/src/Examples/NoEntityFrameworkExample/Controllers/CustomTodoItemsController.cs similarity index 100% rename from src/NoEntityFrameworkExample/Controllers/CustomTodoItemsController.cs rename to src/Examples/NoEntityFrameworkExample/Controllers/CustomTodoItemsController.cs diff --git a/src/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj b/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj similarity index 82% rename from src/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj rename to src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj index f52ec586ff..f2e6d5ec82 100755 --- a/src/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj +++ b/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj @@ -11,8 +11,8 @@ - - + + diff --git a/src/NoEntityFrameworkExample/Program.cs b/src/Examples/NoEntityFrameworkExample/Program.cs similarity index 100% rename from src/NoEntityFrameworkExample/Program.cs rename to src/Examples/NoEntityFrameworkExample/Program.cs diff --git a/src/NoEntityFrameworkExample/Properties/launchSettings.json b/src/Examples/NoEntityFrameworkExample/Properties/launchSettings.json similarity index 100% rename from src/NoEntityFrameworkExample/Properties/launchSettings.json rename to src/Examples/NoEntityFrameworkExample/Properties/launchSettings.json diff --git a/src/NoEntityFrameworkExample/Services/TodoItemService.cs b/src/Examples/NoEntityFrameworkExample/Services/TodoItemService.cs similarity index 100% rename from src/NoEntityFrameworkExample/Services/TodoItemService.cs rename to src/Examples/NoEntityFrameworkExample/Services/TodoItemService.cs diff --git a/src/NoEntityFrameworkExample/Startup.cs b/src/Examples/NoEntityFrameworkExample/Startup.cs similarity index 100% rename from src/NoEntityFrameworkExample/Startup.cs rename to src/Examples/NoEntityFrameworkExample/Startup.cs diff --git a/src/NoEntityFrameworkExample/appsettings.json b/src/Examples/NoEntityFrameworkExample/appsettings.json similarity index 100% rename from src/NoEntityFrameworkExample/appsettings.json rename to src/Examples/NoEntityFrameworkExample/appsettings.json diff --git a/src/Examples/ReportsExample/.bowerrc b/src/Examples/ReportsExample/.bowerrc new file mode 100644 index 0000000000..78d4e9d826 --- /dev/null +++ b/src/Examples/ReportsExample/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "wwwroot/lib" +} diff --git a/src/Examples/ReportsExample/.gitignore b/src/Examples/ReportsExample/.gitignore new file mode 100644 index 0000000000..0ca27f04e1 --- /dev/null +++ b/src/Examples/ReportsExample/.gitignore @@ -0,0 +1,234 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Microsoft Azure ApplicationInsights config file +ApplicationInsights.config + +# Windows Store app package directory +AppPackages/ +BundleArtifacts/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe + +# FAKE - F# Make +.fake/ diff --git a/src/Examples/ReportsExample/Controllers/ReportsController.cs b/src/Examples/ReportsExample/Controllers/ReportsController.cs new file mode 100644 index 0000000000..523ad417bd --- /dev/null +++ b/src/Examples/ReportsExample/Controllers/ReportsController.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; + +namespace ReportsExample.Controllers +{ + [Route("api/[controller]")] + public class ReportsController : BaseJsonApiController + { + public ReportsController( + IJsonApiContext jsonApiContext, + IGetAllService getAll) + : base(jsonApiContext, getAll: getAll) + { } + + [HttpGet] + public override async Task GetAsync() => await base.GetAsync(); + } +} diff --git a/src/Examples/ReportsExample/Models/Report.cs b/src/Examples/ReportsExample/Models/Report.cs new file mode 100644 index 0000000000..39c07aca3d --- /dev/null +++ b/src/Examples/ReportsExample/Models/Report.cs @@ -0,0 +1,15 @@ +using JsonApiDotNetCore.Models; + +public class Report : Identifiable +{ + [Attr("title")] + public string Title { get; set; } + + [Attr("complex-type")] + public ComplexType ComplexType { get; set; } +} + +public class ComplexType +{ + public string CompoundPropertyName { get; set; } +} \ No newline at end of file diff --git a/src/Examples/ReportsExample/Program.cs b/src/Examples/ReportsExample/Program.cs new file mode 100644 index 0000000000..41d4c37780 --- /dev/null +++ b/src/Examples/ReportsExample/Program.cs @@ -0,0 +1,20 @@ +using System.IO; +using Microsoft.AspNetCore.Hosting; + +namespace ReportsExample +{ + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/src/Examples/ReportsExample/ReportsExample.csproj b/src/Examples/ReportsExample/ReportsExample.csproj new file mode 100644 index 0000000000..f50935681a --- /dev/null +++ b/src/Examples/ReportsExample/ReportsExample.csproj @@ -0,0 +1,26 @@ + + + + netcoreapp1.0 + 1.1.1 + $(PackageTargetFallback);dotnet5.6;portable-net45+win8 + + + + + + + + + + + + + + + + + + + + diff --git a/src/Examples/ReportsExample/Services/ReportService.cs b/src/Examples/ReportsExample/Services/ReportService.cs new file mode 100644 index 0000000000..7baffc6174 --- /dev/null +++ b/src/Examples/ReportsExample/Services/ReportService.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +public class ReportService : IGetAllService +{ + private ILogger _logger; + + public ReportService(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public Task> GetAsync() + { + _logger.LogError("GetAsync"); + + var task = new Task>(() => Get()); + + task.RunSynchronously(TaskScheduler.Default); + + return task; + } + + private IEnumerable Get() + { + return new List { + new Report { + Title = "My Report", + ComplexType = new ComplexType { + CompoundPropertyName = "value" + } + } + }; + } +} \ No newline at end of file diff --git a/src/Examples/ReportsExample/Startup.cs b/src/Examples/ReportsExample/Startup.cs new file mode 100644 index 0000000000..39d740da84 --- /dev/null +++ b/src/Examples/ReportsExample/Startup.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace ReportsExample +{ + public class Startup + { + public readonly IConfiguration Config; + + public Startup(IHostingEnvironment env) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddEnvironmentVariables(); + + Config = builder.Build(); + } + + public void ConfigureServices(IServiceCollection services) + { + var mvcBuilder = services.AddMvc(); + services.AddJsonApi(opt => + { + opt.BuildContextGraph(builder => { + builder.AddResource("reports"); + }); + opt.Namespace = "api"; + }, mvcBuilder); + + services.AddScoped, ReportService>(); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + app.UseMvc(); + } + } +} diff --git a/src/Examples/ReportsExample/appsettings.json b/src/Examples/ReportsExample/appsettings.json new file mode 100644 index 0000000000..125f7a4ae9 --- /dev/null +++ b/src/Examples/ReportsExample/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Information" + } + } +} diff --git a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs index fec71f97d6..c176d7b6cb 100644 --- a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs @@ -12,6 +12,8 @@ public class ContextGraphBuilder : IContextGraphBuilder { private List _entities; private bool _usesDbContext; + public Link DocumentLinks { get; set; } = Link.All; + public ContextGraphBuilder() { _entities = new List(); @@ -35,10 +37,20 @@ public void AddResource(string pluralizedTypeName) where TResource : EntityName = pluralizedTypeName, EntityType = entityType, Attributes = GetAttributes(entityType), - Relationships = GetRelationships(entityType) + Relationships = GetRelationships(entityType), + Links = GetLinkFlags(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) { var attributes = new List(); @@ -114,7 +126,7 @@ public void AddDbContext() where T : DbContext private string GetResourceName(PropertyInfo property) { var resourceAttribute = property.GetCustomAttribute(typeof(ResourceAttribute)); - if(resourceAttribute == null) + if (resourceAttribute == null) return property.Name.Dasherize(); return ((ResourceAttribute)resourceAttribute).ResourceName; diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 4662a37668..7e864a03e0 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -33,10 +33,12 @@ public Document Build(IIdentifiable entity) var document = new Document { Data = GetData(contextEntity, entity), - Meta = GetMeta(entity), - Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext)) + Meta = GetMeta(entity) }; + if(ShouldIncludePageLinks(contextEntity)) + document.Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext)); + document.Included = AppendIncludedObject(document.Included, contextEntity, entity); return document; @@ -54,10 +56,12 @@ public Documents Build(IEnumerable entities) var documents = new Documents { Data = new List(), - Meta = GetMeta(enumeratedEntities.FirstOrDefault()), - Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext)) + 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)); @@ -87,6 +91,8 @@ private Dictionary GetMeta(IIdentifiable entity) return null; } + private bool ShouldIncludePageLinks(ContextEntity entity) => entity.Links.HasFlag(Link.Paging); + private List AppendIncludedObject(List includedObject, ContextEntity contextEntity, IIdentifiable entity) { var includedEntities = GetIncludedEntities(contextEntity, entity); @@ -139,14 +145,17 @@ private void AddRelationships(DocumentData data, ContextEntity contextEntity, II contextEntity.Relationships.ForEach(r => { - var relationshipData = new RelationshipData + var relationshipData = new RelationshipData(); + + if(r.DocumentLinks.HasFlag(Link.None) == false) { - Links = new Links - { - Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.StringId, r.PublicRelationshipName), - Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.StringId, r.PublicRelationshipName) - } - }; + relationshipData.Links = new Links(); + if(r.DocumentLinks.HasFlag(Link.Self)) + relationshipData.Links.Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.StringId, r.PublicRelationshipName); + + if(r.DocumentLinks.HasFlag(Link.Related)) + relationshipData.Links.Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.StringId, r.PublicRelationshipName); + } if (RelationshipIsIncluded(r.PublicRelationshipName)) { diff --git a/src/JsonApiDotNetCore/Builders/IContextGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/IContextGraphBuilder.cs index 9249615a40..bab62cca64 100644 --- a/src/JsonApiDotNetCore/Builders/IContextGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/IContextGraphBuilder.cs @@ -1,10 +1,12 @@ using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; using Microsoft.EntityFrameworkCore; namespace JsonApiDotNetCore.Builders { public interface IContextGraphBuilder { + Link DocumentLinks { get; set; } IContextGraph Build(); void AddResource(string pluralizedTypeName) where TResource : class; void AddDbContext() where T : DbContext; diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index e3e5dd21da..2776ebae81 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -1,7 +1,9 @@ using System; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Serialization; using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Configuration { @@ -12,6 +14,7 @@ public class JsonApiOptions public bool IncludeTotalRecordCount { get; set; } public bool AllowClientGeneratedIds { get; set; } public IContextGraph ContextGraph { get; set; } + public IContractResolver JsonContractResolver { get; set; } = new DasherizedResolver(); public void BuildContextGraph(Action builder) where TContext : DbContext diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs new file mode 100644 index 0000000000..a10ea381de --- /dev/null +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -0,0 +1,198 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.Controllers +{ + public class BaseJsonApiController + : BaseJsonApiController + where T : class, IIdentifiable + { + public BaseJsonApiController( + IJsonApiContext jsonApiContext, + IResourceService resourceService + ) : base(jsonApiContext, resourceService) { } + + public BaseJsonApiController( + IJsonApiContext jsonApiContext, + IResourceQueryService queryService = null, + IResourceCmdService cmdService = null + ) : base(jsonApiContext, queryService, cmdService) { } + + public BaseJsonApiController( + IJsonApiContext jsonApiContext, + IGetAllService getAll = null, + IGetByIdService getById = null, + IGetRelationshipService getRelationship = null, + IGetRelationshipsService getRelationships = null, + ICreateService create = null, + IUpdateService update = null, + IUpdateRelationshipService updateRelationships = null, + IDeleteService delete = null + ) : base(jsonApiContext, getAll, getById, getRelationship, getRelationships, create, update, updateRelationships, delete) { } + } + + public class BaseJsonApiController + : JsonApiControllerMixin + where T : class, IIdentifiable + { + private readonly IGetAllService _getAll; + private readonly IGetByIdService _getById; + private readonly IGetRelationshipService _getRelationship; + private readonly IGetRelationshipsService _getRelationships; + private readonly ICreateService _create; + private readonly IUpdateService _update; + private readonly IUpdateRelationshipService _updateRelationships; + private readonly IDeleteService _delete; + private readonly IJsonApiContext _jsonApiContext; + + public BaseJsonApiController( + IJsonApiContext jsonApiContext, + IResourceService resourceService) + { + _jsonApiContext = jsonApiContext.ApplyContext(this); + _getAll = resourceService; + _getById = resourceService; + _getRelationship = resourceService; + _getRelationships = resourceService; + _create = resourceService; + _update = resourceService; + _updateRelationships = resourceService; + _delete = resourceService; + } + + public BaseJsonApiController( + IJsonApiContext jsonApiContext, + IResourceQueryService queryService = null, + IResourceCmdService cmdService = null) + { + _jsonApiContext = jsonApiContext.ApplyContext(this); + _getAll = queryService; + _getById = queryService; + _getRelationship = queryService; + _getRelationships = queryService; + _create = cmdService; + _update = cmdService; + _updateRelationships = cmdService; + _delete = 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) + { + _jsonApiContext = jsonApiContext.ApplyContext(this); + _getAll = getAll; + _getById = getById; + _getRelationship = getRelationship; + _getRelationships = getRelationships; + _create = create; + _update = update; + _updateRelationships = updateRelationships; + _delete = delete; + } + + public virtual async Task GetAsync() + { + if (_getAll == null) throw new JsonApiException(405, "Get requests are not supported"); + + var entities = await _getAll.GetAsync(); + + return Ok(entities); + } + + public virtual async Task GetAsync(TId id) + { + if (_getById == null) throw new JsonApiException(405, "Get by Id requests are not supported"); + + 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 new JsonApiException(405, "Get Relationships requests are not supported"); + + var relationship = await _getRelationships.GetRelationshipsAsync(id, relationshipName); + if (relationship == null) + return NotFound(); + + return Ok(relationship); + } + + public virtual async Task GetRelationshipAsync(TId id, string relationshipName) + { + if (_getRelationship == null) throw new JsonApiException(405, "Get Relationship requests are not supported"); + + var relationship = await _getRelationship.GetRelationshipAsync(id, relationshipName); + + return Ok(relationship); + } + + public virtual async Task PostAsync([FromBody] T entity) + { + if (_create == null) throw new JsonApiException(405, "Post requests are not supported"); + + if (entity == null) + return UnprocessableEntity(); + + if (!_jsonApiContext.Options.AllowClientGeneratedIds && !string.IsNullOrEmpty(entity.StringId)) + return Forbidden(); + + entity = await _create.CreateAsync(entity); + + return Created($"{HttpContext.Request.Path}/{entity.Id}", entity); + } + + public virtual async Task PatchAsync(TId id, [FromBody] T entity) + { + if (_update == null) throw new JsonApiException(405, "Patch requests are not supported"); + + if (entity == null) + return UnprocessableEntity(); + + var updatedEntity = await _update.UpdateAsync(id, entity); + + if (updatedEntity == null) + return NotFound(); + + return Ok(updatedEntity); + } + + public virtual async Task PatchRelationshipsAsync(TId id, string relationshipName, [FromBody] List relationships) + { + if (_updateRelationships == null) throw new JsonApiException(405, "Relationship Patch requests are not supported"); + + await _updateRelationships.UpdateRelationshipsAsync(id, relationshipName, relationships); + + return Ok(); + } + + public virtual async Task DeleteAsync(TId id) + { + if (_delete == null) throw new JsonApiException(405, "Delete requests are not supported"); + + var wasDeleted = await _delete.DeleteAsync(id); + + if (!wasDeleted) + return NotFound(); + + return NoContent(); + } + } +} diff --git a/src/JsonApiDotNetCore/Controllers/DisableQueryAttribute.cs b/src/JsonApiDotNetCore/Controllers/DisableQueryAttribute.cs new file mode 100644 index 0000000000..40ebf385fe --- /dev/null +++ b/src/JsonApiDotNetCore/Controllers/DisableQueryAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace JsonApiDotNetCore.Controllers +{ + public class DisableQueryAttribute : Attribute + { + public DisableQueryAttribute(QueryParams queryParams) + { + QueryParams = queryParams; + } + + public QueryParams QueryParams { get; set; } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Controllers/HttpMethodRestrictionFilter.cs b/src/JsonApiDotNetCore/Controllers/HttpMethodRestrictionFilter.cs index 3ef96c26f5..9bf533502a 100644 --- a/src/JsonApiDotNetCore/Controllers/HttpMethodRestrictionFilter.cs +++ b/src/JsonApiDotNetCore/Controllers/HttpMethodRestrictionFilter.cs @@ -19,7 +19,7 @@ public override async Task OnActionExecutionAsync( var method = context.HttpContext.Request.Method; if(CanExecuteAction(method) == false) - throw new JsonApiException("405", $"This resource does not support {method} requests."); + throw new JsonApiException(405, $"This resource does not support {method} requests."); await next(); } diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs new file mode 100644 index 0000000000..20e5445ebb --- /dev/null +++ b/src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.Controllers +{ + public class JsonApiCmdController + : JsonApiCmdController where T : class, IIdentifiable + { + public JsonApiCmdController( + IJsonApiContext jsonApiContext, + IResourceService resourceService) + : base(jsonApiContext, resourceService) + { } + } + + public class JsonApiCmdController + : BaseJsonApiController where T : class, IIdentifiable + { + public JsonApiCmdController( + IJsonApiContext jsonApiContext, + IResourceService resourceService) + : base(jsonApiContext, resourceService) + { } + + [HttpPost] + public override async Task PostAsync([FromBody] T entity) + => await base.PostAsync(entity); + + [HttpPatch("{id}")] + public override async Task PatchAsync(TId id, [FromBody] T entity) + => await base.PatchAsync(id, entity); + + [HttpPatch("{id}/relationships/{relationshipName}")] + public override async Task PatchRelationshipsAsync( + TId id, string relationshipName, [FromBody] List relationships) + => await base.PatchRelationshipsAsync(id, relationshipName, relationships); + + [HttpDelete("{id}")] + public override async Task DeleteAsync(TId id) => await base.DeleteAsync(id); + } +} diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs index 094a74f2d3..bea2105482 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs @@ -19,109 +19,49 @@ public JsonApiController( } public class JsonApiController - : JsonApiControllerMixin where T : class, IIdentifiable + : BaseJsonApiController where T : class, IIdentifiable { - private readonly ILogger _logger; - private readonly IResourceService _resourceService; - private readonly IJsonApiContext _jsonApiContext; - public JsonApiController( IJsonApiContext jsonApiContext, IResourceService resourceService, - ILoggerFactory loggerFactory) - { - _jsonApiContext = jsonApiContext.ApplyContext(); - _resourceService = resourceService; - _logger = loggerFactory.CreateLogger>(); - } + ILoggerFactory loggerFactory) + : base(jsonApiContext, resourceService) + { } public JsonApiController( IJsonApiContext jsonApiContext, IResourceService resourceService) - { - _jsonApiContext = jsonApiContext.ApplyContext(); - _resourceService = resourceService; - } + : base(jsonApiContext, resourceService) + { } [HttpGet] - public virtual async Task GetAsync() - { - var entities = await _resourceService.GetAsync(); - return Ok(entities); - } + public override async Task GetAsync() => await base.GetAsync(); [HttpGet("{id}")] - public virtual async Task GetAsync(TId id) - { - var entity = await _resourceService.GetAsync(id); - - if (entity == null) - return NotFound(); - - return Ok(entity); - } + public override async Task GetAsync(TId id) => await base.GetAsync(id); [HttpGet("{id}/relationships/{relationshipName}")] - public virtual async Task GetRelationshipsAsync(TId id, string relationshipName) - { - var relationship = await _resourceService.GetRelationshipsAsync(id, relationshipName); - if(relationship == null) - return NotFound(); - - return Ok(relationship); - } + public override async Task GetRelationshipsAsync(TId id, string relationshipName) + => await base.GetRelationshipsAsync(id, relationshipName); [HttpGet("{id}/{relationshipName}")] - public virtual async Task GetRelationshipAsync(TId id, string relationshipName) - { - var relationship = await _resourceService.GetRelationshipAsync(id, relationshipName); - return Ok(relationship); - } + public override async Task GetRelationshipAsync(TId id, string relationshipName) + => await base.GetRelationshipAsync(id, relationshipName); [HttpPost] - public virtual async Task PostAsync([FromBody] T entity) - { - if (entity == null) - return UnprocessableEntity(); - - if (!_jsonApiContext.Options.AllowClientGeneratedIds && !string.IsNullOrEmpty(entity.StringId)) - return Forbidden(); - - entity = await _resourceService.CreateAsync(entity); - - return Created($"{HttpContext.Request.Path}/{entity.Id}", entity); - } + public override async Task PostAsync([FromBody] T entity) + => await base.PostAsync(entity); [HttpPatch("{id}")] - public virtual async Task PatchAsync(TId id, [FromBody] T entity) - { - if (entity == null) - return UnprocessableEntity(); - - var updatedEntity = await _resourceService.UpdateAsync(id, entity); - - if(updatedEntity == null) - return NotFound(); - - return Ok(updatedEntity); - } + public override async Task PatchAsync(TId id, [FromBody] T entity) + => await base.PatchAsync(id, entity); [HttpPatch("{id}/relationships/{relationshipName}")] - public virtual async Task PatchRelationshipsAsync(TId id, string relationshipName, [FromBody] List relationships) - { - await _resourceService.UpdateRelationshipsAsync(id, relationshipName, relationships); - return Ok(); - } + public override async Task PatchRelationshipsAsync( + TId id, string relationshipName, [FromBody] List relationships) + => await base.PatchRelationshipsAsync(id, relationshipName, relationships); [HttpDelete("{id}")] - public virtual async Task DeleteAsync(TId id) - { - var wasDeleted = await _resourceService.DeleteAsync(id); - - if (!wasDeleted) - return NotFound(); - - return NoContent(); - } + public override async Task DeleteAsync(TId id) => await base.DeleteAsync(id); } } diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs new file mode 100644 index 0000000000..3e78bc4f8f --- /dev/null +++ b/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.Controllers +{ + public class JsonApiQueryController + : JsonApiQueryController where T : class, IIdentifiable + { + public JsonApiQueryController( + IJsonApiContext jsonApiContext, + IResourceService resourceService) + : base(jsonApiContext, resourceService) + { } + } + + public class JsonApiQueryController + : BaseJsonApiController where T : class, IIdentifiable + { + public JsonApiQueryController( + IJsonApiContext jsonApiContext, + IResourceService resourceService) + : base(jsonApiContext, resourceService) + { } + + [HttpGet] + public override async Task GetAsync() => await base.GetAsync(); + + [HttpGet("{id}")] + public override async Task GetAsync(TId id) => await base.GetAsync(id); + + [HttpGet("{id}/relationships/{relationshipName}")] + public override async Task GetRelationshipsAsync(TId id, string relationshipName) + => await base.GetRelationshipsAsync(id, relationshipName); + + [HttpGet("{id}/{relationshipName}")] + public override async Task GetRelationshipAsync(TId id, string relationshipName) + => await base.GetRelationshipAsync(id, relationshipName); + } +} diff --git a/src/JsonApiDotNetCore/Controllers/QueryParams.cs b/src/JsonApiDotNetCore/Controllers/QueryParams.cs new file mode 100644 index 0000000000..7e59f976c5 --- /dev/null +++ b/src/JsonApiDotNetCore/Controllers/QueryParams.cs @@ -0,0 +1,15 @@ +using System; + +namespace JsonApiDotNetCore.Controllers +{ + public enum QueryParams + { + Filter = 1 << 0, + Sort = 1 << 1, + Include = 1 << 2, + Page = 1 << 3, + Fields = 1 << 4, + All = ~(-1 << 5), + None = 1 << 6, + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 952b6f8326..bc2d9b0661 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -18,14 +18,6 @@ public class DefaultEntityRepository IEntityRepository where TEntity : class, IIdentifiable { - [Obsolete("DbContext is no longer directly injected into the ctor. Use JsonApiContext.GetDbContextResolver() instead")] - public DefaultEntityRepository( - DbContext context, - ILoggerFactory loggerFactory, - IJsonApiContext jsonApiContext) - : base(context, loggerFactory, jsonApiContext) - { } - public DefaultEntityRepository( ILoggerFactory loggerFactory, IJsonApiContext jsonApiContext) @@ -170,7 +162,7 @@ public virtual IQueryable Include(IQueryable entities, string if(relationship != null) return entities.Include(relationship.InternalRelationshipName); - throw new JsonApiException("400", $"Invalid relationship {relationshipName} on {entity.EntityName}", + throw new JsonApiException(400, $"Invalid relationship {relationshipName} on {entity.EntityName}", $"{entity.EntityName} does not have a relationship named {relationshipName}"); } diff --git a/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs b/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs new file mode 100644 index 0000000000..aad16a9efc --- /dev/null +++ b/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Data +{ + public interface IEntityReadRepository + : IEntityReadRepository + where TEntity : class, IIdentifiable + { } + + public interface IEntityReadRepository + where TEntity : class, IIdentifiable + { + IQueryable Get(); + + IQueryable Include(IQueryable entities, string relationshipName); + + IQueryable Filter(IQueryable entities, FilterQuery filterQuery); + + IQueryable Sort(IQueryable entities, List sortQueries); + + Task> PageAsync(IQueryable entities, int pageSize, int pageNumber); + + Task GetAsync(TId id); + + Task GetAndIncludeAsync(TId id, string relationshipName); + } +} diff --git a/src/JsonApiDotNetCore/Data/IEntityRepository.cs b/src/JsonApiDotNetCore/Data/IEntityRepository.cs index 547c930b8d..4c35d6ea3f 100644 --- a/src/JsonApiDotNetCore/Data/IEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/IEntityRepository.cs @@ -6,35 +6,14 @@ namespace JsonApiDotNetCore.Data { - public interface IEntityRepository + public interface IEntityRepository : IEntityRepository where TEntity : class, IIdentifiable - { - } + { } public interface IEntityRepository + : IEntityReadRepository, + IEntityWriteRepository where TEntity : class, IIdentifiable - { - IQueryable Get(); - - IQueryable Include(IQueryable entities, string relationshipName); - - IQueryable Filter(IQueryable entities, FilterQuery filterQuery); - - IQueryable Sort(IQueryable entities, List sortQueries); - - Task> PageAsync(IQueryable entities, int pageSize, int pageNumber); - - Task GetAsync(TId id); - - Task GetAndIncludeAsync(TId id, string relationshipName); - - Task CreateAsync(TEntity entity); - - Task UpdateAsync(TId id, TEntity entity); - - Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds); - - Task DeleteAsync(TId id); - } + { } } diff --git a/src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs b/src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs new file mode 100644 index 0000000000..37c60b8521 --- /dev/null +++ b/src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Data +{ + public interface IEntityWriteRepository + : IEntityWriteRepository + where TEntity : class, IIdentifiable + { } + + public interface IEntityWriteRepository + where TEntity : class, IIdentifiable + { + Task CreateAsync(TEntity entity); + + Task UpdateAsync(TId id, TEntity entity); + + Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds); + + Task DeleteAsync(TId id); + } +} diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index f5a1db4010..c40d61b517 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -93,7 +93,7 @@ public static IQueryable Filter(this IQueryable sourc } catch (FormatException) { - throw new JsonApiException("400", $"Could not cast {filterQuery.PropertyValue} to {property.PropertyType.Name}"); + throw new JsonApiException(400, $"Could not cast {filterQuery.PropertyValue} to {property.PropertyType.Name}"); } } @@ -137,7 +137,7 @@ public static IQueryable Filter(this IQueryable sourc } catch (FormatException) { - throw new JsonApiException("400", $"Could not cast {filterQuery.PropertyValue} to {relatedAttr.PropertyType.Name}"); + throw new JsonApiException(400, $"Could not cast {filterQuery.PropertyValue} to {relatedAttr.PropertyType.Name}"); } } @@ -171,7 +171,7 @@ private static Expression GetFilterExpressionLambda(Expression left, Expression body = Expression.Call(left, "Contains", null, right); break; default: - throw new JsonApiException("500", $"Unknown filter operation {operation}"); + throw new JsonApiException(500, $"Unknown filter operation {operation}"); } return body; diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 012f47da2e..418be48946 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -107,6 +107,7 @@ public static void AddJsonApiInternals( services.AddScoped(); services.AddScoped(); services.AddScoped(typeof(GenericProcessor<>)); + services.AddScoped(); } public static void SerializeAsJsonApi(this MvcOptions options, JsonApiOptions jsonApiOptions) diff --git a/src/JsonApiDotNetCore/Internal/ContextEntity.cs b/src/JsonApiDotNetCore/Internal/ContextEntity.cs index e43d85ab0f..4843d245c1 100644 --- a/src/JsonApiDotNetCore/Internal/ContextEntity.cs +++ b/src/JsonApiDotNetCore/Internal/ContextEntity.cs @@ -10,5 +10,6 @@ public class ContextEntity public Type EntityType { get; set; } public List Attributes { get; set; } public List Relationships { get; set; } + public Link Links { get; set; } = Link.All; } } diff --git a/src/JsonApiDotNetCore/Internal/ContextGraph.cs b/src/JsonApiDotNetCore/Internal/ContextGraph.cs index 7881926c23..aae5c2179b 100644 --- a/src/JsonApiDotNetCore/Internal/ContextGraph.cs +++ b/src/JsonApiDotNetCore/Internal/ContextGraph.cs @@ -33,7 +33,7 @@ public object GetRelationship(TParent entity, string relationshipName) .FirstOrDefault(p => p.Name.ToLower() == relationshipName.ToLower()); if(navigationProperty == null) - throw new JsonApiException("400", $"{parentEntityType} does not contain a relationship named {relationshipName}"); + throw new JsonApiException(400, $"{parentEntityType} does not contain a relationship named {relationshipName}"); return navigationProperty.GetValue(entity); } diff --git a/src/JsonApiDotNetCore/Internal/Error.cs b/src/JsonApiDotNetCore/Internal/Error.cs index c47f7276b2..0443e1edb7 100644 --- a/src/JsonApiDotNetCore/Internal/Error.cs +++ b/src/JsonApiDotNetCore/Internal/Error.cs @@ -1,3 +1,4 @@ +using System; using Newtonsoft.Json; namespace JsonApiDotNetCore.Internal @@ -13,12 +14,25 @@ public Error(string status, string title) Title = title; } + public Error(int status, string title) + { + Status = status.ToString(); + Title = title; + } + public Error(string status, string title, string detail) { Status = status; Title = title; Detail = detail; } + + public Error(int status, string title, string detail) + { + Status = status.ToString(); + Title = title; + Detail = detail; + } [JsonProperty("title")] public string Title { get; set; } diff --git a/src/JsonApiDotNetCore/Internal/JsonApiException.cs b/src/JsonApiDotNetCore/Internal/JsonApiException.cs index e62503c379..9ce12fe428 100644 --- a/src/JsonApiDotNetCore/Internal/JsonApiException.cs +++ b/src/JsonApiDotNetCore/Internal/JsonApiException.cs @@ -14,26 +14,31 @@ public JsonApiException(ErrorCollection errorCollection) public JsonApiException(Error error) : base(error.Title) - { - _errors.Add(error); - } + => _errors.Add(error); + [Obsolete("Use int statusCode overload instead")] public JsonApiException(string statusCode, string message) : base(message) - { - _errors.Add(new Error(statusCode, message, null)); - } + => _errors.Add(new Error(statusCode, message, null)); + [Obsolete("Use int statusCode overload instead")] public JsonApiException(string statusCode, string message, string detail) : base(message) - { - _errors.Add(new Error(statusCode, message, detail)); - } + => _errors.Add(new Error(statusCode, message, detail)); - public ErrorCollection GetError() - { - return _errors; - } + public JsonApiException(int statusCode, string message) + : base(message) + => _errors.Add(new Error(statusCode, message, null)); + + public JsonApiException(int statusCode, string message, string detail) + : base(message) + => _errors.Add(new Error(statusCode, message, detail)); + + public JsonApiException(int statusCode, string message, Exception innerException) + : base(message, innerException) + => _errors.Add(new Error(statusCode, message, innerException.Message)); + + public ErrorCollection GetError() => _errors; public int GetStatusCode() { diff --git a/src/JsonApiDotNetCore/Internal/JsonApiExceptionFactory.cs b/src/JsonApiDotNetCore/Internal/JsonApiExceptionFactory.cs index 7e251ebae0..42f3037f89 100644 --- a/src/JsonApiDotNetCore/Internal/JsonApiExceptionFactory.cs +++ b/src/JsonApiDotNetCore/Internal/JsonApiExceptionFactory.cs @@ -13,9 +13,9 @@ public static JsonApiException GetException(Exception exception) case "JsonApiException": return (JsonApiException)exception; case "InvalidCastException": - return new JsonApiException("409", exception.Message); + return new JsonApiException(409, exception.Message); default: - return new JsonApiException("500", exception.Message, GetExceptionDetail(exception.InnerException)); + return new JsonApiException(500, exception.Message, GetExceptionDetail(exception.InnerException)); } } diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs index 5a17b59d2a..8af2fe95e1 100644 --- a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs @@ -16,7 +16,7 @@ public AttrFilterQuery( var attribute = GetAttribute(filterQuery.Key); - FilteredAttribute = attribute ?? throw new JsonApiException("400", $"{filterQuery.Key} is not a valid property."); + FilteredAttribute = attribute ?? throw new JsonApiException(400, $"{filterQuery.Key} is not a valid property."); PropertyValue = filterQuery.Value; FilterOperation = GetFilterOperation(filterQuery.Operation); } diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs index 3a677bb247..527b842ca8 100644 --- a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs @@ -9,7 +9,7 @@ protected FilterOperations GetFilterOperation(string prefix) if (prefix.Length == 0) return FilterOperations.eq; if (!Enum.TryParse(prefix, out FilterOperations opertion)) - throw new JsonApiException("400", $"Invalid filter prefix '{prefix}'"); + throw new JsonApiException(400, $"Invalid filter prefix '{prefix}'"); return opertion; } diff --git a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs b/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs index 9bcdbe0179..f4e1965efe 100644 --- a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs +++ b/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs @@ -5,6 +5,7 @@ using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Controllers; namespace JsonApiDotNetCore.Internal.Query { @@ -13,57 +14,61 @@ public class QuerySet private readonly IJsonApiContext _jsonApiContext; public QuerySet( - IJsonApiContext jsonApiContext, + IJsonApiContext jsonApiContext, IQueryCollection query) { _jsonApiContext = jsonApiContext; - PageQuery = new PageQuery(); - Filters = new List(); - Fields = new List(); BuildQuerySet(query); } - public List Filters { get; set; } - public PageQuery PageQuery { get; set; } - public List SortParameters { get; set; } - public List IncludedRelationships { get; set; } - public List Fields { get; set; } + public List Filters { get; set; } = new List(); + public PageQuery PageQuery { get; set; } = new PageQuery(); + public List SortParameters { get; set; } = new List(); + public List IncludedRelationships { get; set; } = new List(); + public List Fields { get; set; } = new List(); private void BuildQuerySet(IQueryCollection query) { + var disabledQueries = _jsonApiContext.GetControllerAttribute()?.QueryParams ?? QueryParams.None; + foreach (var pair in query) { if (pair.Key.StartsWith("filter")) { - Filters.AddRange(ParseFilterQuery(pair.Key, pair.Value)); + if (disabledQueries.HasFlag(QueryParams.Filter) == false) + Filters.AddRange(ParseFilterQuery(pair.Key, pair.Value)); continue; } if (pair.Key.StartsWith("sort")) { - SortParameters = ParseSortParameters(pair.Value); + if (disabledQueries.HasFlag(QueryParams.Sort) == false) + SortParameters = ParseSortParameters(pair.Value); continue; } if (pair.Key.StartsWith("include")) { - IncludedRelationships = ParseIncludedRelationships(pair.Value); + if (disabledQueries.HasFlag(QueryParams.Include) == false) + IncludedRelationships = ParseIncludedRelationships(pair.Value); continue; } if (pair.Key.StartsWith("page")) { - PageQuery = ParsePageQuery(pair.Key, pair.Value); + if (disabledQueries.HasFlag(QueryParams.Page) == false) + PageQuery = ParsePageQuery(pair.Key, pair.Value); continue; } if (pair.Key.StartsWith("fields")) { - Fields = ParseFieldsQuery(pair.Key, pair.Value); + if (disabledQueries.HasFlag(QueryParams.Fields) == false) + Fields = ParseFieldsQuery(pair.Key, pair.Value); continue; } - throw new JsonApiException("400", $"{pair} is not a valid query."); + throw new JsonApiException(400, $"{pair} is not a valid query."); } } @@ -74,9 +79,9 @@ private List ParseFilterQuery(string key, string value) var queries = new List(); var propertyName = key.Split('[', ']')[1].ToProperCase(); - + var values = value.Split(','); - foreach(var val in values) + foreach (var val in values) { (var operation, var filterValue) = ParseFilterOperation(val); queries.Add(new FilterQuery(propertyName, filterValue, operation)); @@ -87,14 +92,14 @@ private List ParseFilterQuery(string key, string value) private (string operation, string value) ParseFilterOperation(string value) { - if(value.Length < 3) + if (value.Length < 3) return (string.Empty, value); - + var operation = value.Split(':'); - if(operation.Length == 1) + if (operation.Length == 1) return (string.Empty, value); - + // remove prefix from value var prefix = operation[0]; value = operation[1]; @@ -109,7 +114,7 @@ private PageQuery ParsePageQuery(string key, string value) PageQuery = PageQuery ?? new PageQuery(); var propertyName = key.Split('[', ']')[1]; - + if (propertyName == "size") PageQuery.PageSize = Convert.ToInt32(value); else if (propertyName == "number") @@ -142,8 +147,8 @@ private List ParseSortParameters(string value) private List ParseIncludedRelationships(string value) { - if(value.Contains(".")) - throw new JsonApiException("400", "Deeply nested relationships are not supported"); + if (value.Contains(".")) + throw new JsonApiException(400, "Deeply nested relationships are not supported"); return value .Split(',') @@ -157,11 +162,11 @@ private List ParseFieldsQuery(string key, string value) var includedFields = new List { "Id" }; - if(typeName != _jsonApiContext.RequestEntity.EntityName) + if (typeName != _jsonApiContext.RequestEntity.EntityName) return includedFields; var fields = value.Split(','); - foreach(var field in fields) + foreach (var field in fields) { var internalAttrName = _jsonApiContext.RequestEntity .Attributes diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs index e4168ad08b..dc633c5302 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs @@ -18,12 +18,12 @@ public RelatedAttrFilterQuery( var relationship = GetRelationship(relationshipArray[0]); if (relationship == null) - throw new JsonApiException("400", $"{relationshipArray[0]} is not a valid relationship."); + throw new JsonApiException(400, $"{relationshipArray[0]} is not a valid relationship."); var attribute = GetAttribute(relationship, relationshipArray[1]); FilteredRelationship = relationship; - FilteredAttribute = attribute ?? throw new JsonApiException("400", $"{relationshipArray[1]} is not a valid attribute on {relationshipArray[0]}."); + FilteredAttribute = attribute ?? throw new JsonApiException(400, $"{relationshipArray[1]} is not a valid attribute on {relationshipArray[0]}."); PropertyValue = filterQuery.Value; FilterOperation = GetFilterOperation(filterQuery.Operation); } diff --git a/src/JsonApiDotNetCore/Internal/TypeHelper.cs b/src/JsonApiDotNetCore/Internal/TypeHelper.cs index ca382c10d6..cc64b398dd 100644 --- a/src/JsonApiDotNetCore/Internal/TypeHelper.cs +++ b/src/JsonApiDotNetCore/Internal/TypeHelper.cs @@ -7,15 +7,23 @@ public static class TypeHelper { public static object ConvertType(object value, Type type) { + if (value == null) + return null; + + var valueType = value.GetType(); + try { - if (value == null) - return null; + if (valueType == type || type.IsAssignableFrom(valueType)) + return value; type = Nullable.GetUnderlyingType(type) ?? type; var stringValue = value.ToString(); + if (string.IsNullOrEmpty(stringValue)) + return GetDefaultType(type); + if (type == typeof(Guid)) return Guid.Parse(stringValue); @@ -29,8 +37,22 @@ public static object ConvertType(object value, Type type) } catch (Exception e) { - throw new FormatException($"{ value } cannot be converted to { type.GetTypeInfo().Name }", e); + throw new FormatException($"{ valueType } cannot be converted to { type }", e); } } + + private static object GetDefaultType(Type type) + { + if (type.GetTypeInfo().IsValueType) + { + return Activator.CreateInstance(type); + } + return null; + } + + public static T ConvertType(object value) + { + return (T)ConvertType(value, typeof(T)); + } } } diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 01b1879e83..55775d73f5 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,6 +1,6 @@  - 2.0.9 + 2.1.0 netstandard1.6 JsonApiDotNetCore JsonApiDotNetCore diff --git a/src/JsonApiDotNetCore/Models/AttrAttribute.cs b/src/JsonApiDotNetCore/Models/AttrAttribute.cs index 65fcb3d44b..f84fd229ef 100644 --- a/src/JsonApiDotNetCore/Models/AttrAttribute.cs +++ b/src/JsonApiDotNetCore/Models/AttrAttribute.cs @@ -6,19 +6,22 @@ namespace JsonApiDotNetCore.Models { public class AttrAttribute : Attribute { - public AttrAttribute(string publicName) + public AttrAttribute(string publicName, bool isImmutable = false) { PublicAttributeName = publicName; + IsImmutable = isImmutable; } - public AttrAttribute(string publicName, string internalName) + public AttrAttribute(string publicName, string internalName, bool isImmutable = false) { PublicAttributeName = publicName; InternalAttributeName = internalName; + IsImmutable = isImmutable; } public string PublicAttributeName { get; set; } public string InternalAttributeName { get; set; } + public bool IsImmutable { get; set; } public object GetValue(object entity) { diff --git a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs index 445b82c22a..379458014b 100644 --- a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs @@ -4,8 +4,8 @@ namespace JsonApiDotNetCore.Models { public class HasManyAttribute : RelationshipAttribute { - public HasManyAttribute(string publicName) - : base(publicName) + public HasManyAttribute(string publicName, Link documentLinks = Link.All) + : base(publicName, documentLinks) { PublicRelationshipName = publicName; } diff --git a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs index 29661de485..296b71369e 100644 --- a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs @@ -4,8 +4,8 @@ namespace JsonApiDotNetCore.Models { public class HasOneAttribute : RelationshipAttribute { - public HasOneAttribute(string publicName) - : base(publicName) + public HasOneAttribute(string publicName, Link documentLinks = Link.All) + : base(publicName, documentLinks) { PublicRelationshipName = publicName; } diff --git a/src/JsonApiDotNetCore/Models/Link.cs b/src/JsonApiDotNetCore/Models/Link.cs new file mode 100644 index 0000000000..2d99fa7197 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/Link.cs @@ -0,0 +1,14 @@ +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/LinksAttribute.cs b/src/JsonApiDotNetCore/Models/LinksAttribute.cs new file mode 100644 index 0000000000..85e2693111 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/LinksAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace JsonApiDotNetCore.Models +{ + public class LinksAttribute : Attribute + { + public LinksAttribute(Link links) + { + Links = links; + } + + public Link Links { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs index aaf065d6be..93662032a5 100644 --- a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs @@ -4,9 +4,10 @@ namespace JsonApiDotNetCore.Models { public abstract class RelationshipAttribute : Attribute { - protected RelationshipAttribute(string publicName) + protected RelationshipAttribute(string publicName, Link documentLinks) { PublicRelationshipName = publicName; + DocumentLinks = documentLinks; } public string PublicRelationshipName { get; set; } @@ -14,6 +15,7 @@ protected RelationshipAttribute(string publicName) public Type Type { get; set; } public bool IsHasMany => GetType() == typeof(HasManyAttribute); public bool IsHasOne => GetType() == typeof(HasOneAttribute); + public Link DocumentLinks { get; set; } = Link.All; public abstract void SetValue(object entity, object newValue); diff --git a/src/JsonApiDotNetCore/Serialization/DasherizedResolver.cs b/src/JsonApiDotNetCore/Serialization/DasherizedResolver.cs new file mode 100644 index 0000000000..1b4a3aae6c --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/DasherizedResolver.cs @@ -0,0 +1,19 @@ +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/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index f9cb803c27..dc7e515561 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -14,14 +14,14 @@ namespace JsonApiDotNetCore.Serialization public class JsonApiDeSerializer : IJsonApiDeSerializer { private readonly IJsonApiContext _jsonApiContext; - private readonly IGenericProcessorFactory _genericProcessorFactor; + private readonly IGenericProcessorFactory _genericProcessorFactory; public JsonApiDeSerializer( IJsonApiContext jsonApiContext, IGenericProcessorFactory genericProcessorFactory) { _jsonApiContext = jsonApiContext; - _genericProcessorFactor = genericProcessorFactory; + _genericProcessorFactory = genericProcessorFactory; } public object Deserialize(string requestBody) @@ -34,14 +34,12 @@ public object Deserialize(string requestBody) } catch (Exception e) { - throw new JsonApiException("400", "Failed to deserialize request body", e.Message); + throw new JsonApiException(400, "Failed to deserialize request body", e); } } - public object Deserialize(string requestBody) - { - return (TEntity)Deserialize(requestBody); - } + public TEntity Deserialize(string requestBody) + => (TEntity)Deserialize(requestBody); public object DeserializeRelationship(string requestBody) { @@ -56,7 +54,7 @@ public object DeserializeRelationship(string requestBody) } catch (Exception e) { - throw new JsonApiException("400", "Failed to deserialize request body", e.Message); + throw new JsonApiException(400, "Failed to deserialize request body", e); } } @@ -77,7 +75,7 @@ public List DeserializeList(string requestBody) } catch (Exception e) { - throw new JsonApiException("400", "Failed to deserialize request body", e.Message); + throw new JsonApiException(400, "Failed to deserialize request body", e); } } @@ -114,18 +112,38 @@ private object SetEntityAttributes( if (entityProperty == null) throw new ArgumentException($"{contextEntity.EntityType.Name} does not contain an attribute named {attr.InternalAttributeName}", nameof(entity)); - object newValue; - if (attributeValues.TryGetValue(attr.PublicAttributeName, out newValue)) + if (attributeValues.TryGetValue(attr.PublicAttributeName, out object newValue)) { - var convertedValue = TypeHelper.ConvertType(newValue, entityProperty.PropertyType); + var convertedValue = ConvertAttrValue(newValue, entityProperty.PropertyType); entityProperty.SetValue(entity, convertedValue); - _jsonApiContext.AttributesToUpdate[attr] = convertedValue; + + if(attr.IsImmutable == false) + _jsonApiContext.AttributesToUpdate[attr] = convertedValue; } } 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) + { + var serializerSettings = new JsonSerializerSettings + { + ContractResolver = _jsonApiContext.Options.JsonContractResolver + }; + + return obj.ToObject(targetType, JsonSerializer.Create(serializerSettings)); + } + private object SetRelationships( object entity, ContextEntity contextEntity, @@ -155,7 +173,7 @@ private object SetHasOneRelationship(object entity, var entityProperty = entityProperties.FirstOrDefault(p => p.Name == $"{attr.InternalRelationshipName}Id"); if (entityProperty == null) - throw new JsonApiException("400", $"{contextEntity.EntityType.Name} does not contain an relationsip named {attr.InternalRelationshipName}"); + throw new JsonApiException(400, $"{contextEntity.EntityType.Name} does not contain an relationsip named {attr.InternalRelationshipName}"); var relationshipName = attr.PublicRelationshipName; @@ -188,7 +206,7 @@ private object SetHasManyRelationship(object entity, var entityProperty = entityProperties.FirstOrDefault(p => p.Name == attr.InternalRelationshipName); if (entityProperty == null) - throw new JsonApiException("400", $"{contextEntity.EntityType.Name} does not contain an relationsip named {attr.InternalRelationshipName}"); + throw new JsonApiException(400, $"{contextEntity.EntityType.Name} does not contain an relationsip named {attr.InternalRelationshipName}"); var relationshipName = attr.PublicRelationshipName; @@ -198,7 +216,7 @@ private object SetHasManyRelationship(object entity, if (data == null) return entity; - var genericProcessor = _genericProcessorFactor.GetProcessor(attr.Type); + var genericProcessor = _genericProcessorFactory.GetProcessor(attr.Type); var ids = relationshipData.ManyData.Select(r => r["id"]); genericProcessor.SetRelationships(entity, attr, ids); } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs index e4fb140d5b..a7e14341b0 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs @@ -84,7 +84,8 @@ private string SerializeDocument(object entity) private string _serialize(object obj) { return JsonConvert.SerializeObject(obj, new JsonSerializerSettings { - NullValueHandling = NullValueHandling.Ignore + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = _jsonApiContext.Options.JsonContractResolver }); } } diff --git a/src/JsonApiDotNetCore/Services/Contract/ICreateService.cs b/src/JsonApiDotNetCore/Services/Contract/ICreateService.cs new file mode 100644 index 0000000000..a4c0cd6cbb --- /dev/null +++ b/src/JsonApiDotNetCore/Services/Contract/ICreateService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Services +{ + public interface ICreateService : ICreateService + where T : class, IIdentifiable + { } + + public interface ICreateService + where T : class, IIdentifiable + { + Task CreateAsync(T entity); + } +} diff --git a/src/JsonApiDotNetCore/Services/Contract/IDeleteService.cs b/src/JsonApiDotNetCore/Services/Contract/IDeleteService.cs new file mode 100644 index 0000000000..4ba09fdf40 --- /dev/null +++ b/src/JsonApiDotNetCore/Services/Contract/IDeleteService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Services +{ + public interface IDeleteService : IDeleteService + where T : class, IIdentifiable + { } + + public interface IDeleteService + where T : class, IIdentifiable + { + Task DeleteAsync(TId id); + } +} diff --git a/src/JsonApiDotNetCore/Services/Contract/IGetAllService.cs b/src/JsonApiDotNetCore/Services/Contract/IGetAllService.cs new file mode 100644 index 0000000000..16b367911c --- /dev/null +++ b/src/JsonApiDotNetCore/Services/Contract/IGetAllService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Services +{ + public interface IGetAllService : IGetAllService + where T : class, IIdentifiable + { } + + public interface IGetAllService + where T : class, IIdentifiable + { + Task> GetAsync(); + } +} diff --git a/src/JsonApiDotNetCore/Services/Contract/IGetByIdService.cs b/src/JsonApiDotNetCore/Services/Contract/IGetByIdService.cs new file mode 100644 index 0000000000..27761abd5d --- /dev/null +++ b/src/JsonApiDotNetCore/Services/Contract/IGetByIdService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Services +{ + public interface IGetByIdService : IGetByIdService + where T : class, IIdentifiable + { } + + public interface IGetByIdService + where T : class, IIdentifiable + { + Task GetAsync(TId id); + } +} diff --git a/src/JsonApiDotNetCore/Services/Contract/IGetRelationshipService.cs b/src/JsonApiDotNetCore/Services/Contract/IGetRelationshipService.cs new file mode 100644 index 0000000000..bd9c0b2be0 --- /dev/null +++ b/src/JsonApiDotNetCore/Services/Contract/IGetRelationshipService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Services +{ + public interface IGetRelationshipService : IGetRelationshipService + where T : class, IIdentifiable + { } + + public interface IGetRelationshipService + where T : class, IIdentifiable + { + Task GetRelationshipAsync(TId id, string relationshipName); + } +} diff --git a/src/JsonApiDotNetCore/Services/Contract/IGetRelationshipsService.cs b/src/JsonApiDotNetCore/Services/Contract/IGetRelationshipsService.cs new file mode 100644 index 0000000000..a61cf8f7ac --- /dev/null +++ b/src/JsonApiDotNetCore/Services/Contract/IGetRelationshipsService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Services +{ + public interface IGetRelationshipsService : IGetRelationshipsService + where T : class, IIdentifiable + { } + + public interface IGetRelationshipsService + where T : class, IIdentifiable + { + Task GetRelationshipsAsync(TId id, string relationshipName); + } +} diff --git a/src/JsonApiDotNetCore/Services/Contract/IResourceCmdService.cs b/src/JsonApiDotNetCore/Services/Contract/IResourceCmdService.cs new file mode 100644 index 0000000000..2633fd589b --- /dev/null +++ b/src/JsonApiDotNetCore/Services/Contract/IResourceCmdService.cs @@ -0,0 +1,16 @@ +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Services +{ + public interface IResourceCmdService : IResourceCmdService + where T : class, IIdentifiable + { } + + public interface IResourceCmdService : + ICreateService, + IUpdateService, + IUpdateRelationshipService, + IDeleteService + where T : class, IIdentifiable + { } +} diff --git a/src/JsonApiDotNetCore/Services/Contract/IResourceQueryService.cs b/src/JsonApiDotNetCore/Services/Contract/IResourceQueryService.cs new file mode 100644 index 0000000000..8a9e247a3f --- /dev/null +++ b/src/JsonApiDotNetCore/Services/Contract/IResourceQueryService.cs @@ -0,0 +1,16 @@ +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Services +{ + public interface IResourceQueryService : IResourceQueryService + where T : class, IIdentifiable + { } + + public interface IResourceQueryService : + IGetAllService, + IGetByIdService, + IGetRelationshipsService, + IGetRelationshipService + where T : class, IIdentifiable + { } +} diff --git a/src/JsonApiDotNetCore/Services/Contract/IResourceService.cs b/src/JsonApiDotNetCore/Services/Contract/IResourceService.cs new file mode 100644 index 0000000000..82f3505c78 --- /dev/null +++ b/src/JsonApiDotNetCore/Services/Contract/IResourceService.cs @@ -0,0 +1,14 @@ +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Services +{ + public interface IResourceService + : IResourceService + where T : class, IIdentifiable + { } + + public interface IResourceService + : IResourceCmdService, IResourceQueryService + where T : class, IIdentifiable + { } +} diff --git a/src/JsonApiDotNetCore/Services/Contract/IUpdateRelationshipService.cs b/src/JsonApiDotNetCore/Services/Contract/IUpdateRelationshipService.cs new file mode 100644 index 0000000000..c7b2fd77bf --- /dev/null +++ b/src/JsonApiDotNetCore/Services/Contract/IUpdateRelationshipService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Services +{ + public interface IUpdateRelationshipService : IUpdateRelationshipService + where T : class, IIdentifiable + { } + + public interface IUpdateRelationshipService + where T : class, IIdentifiable + { + Task UpdateRelationshipsAsync(TId id, string relationshipName, List relationships); + } +} diff --git a/src/JsonApiDotNetCore/Services/Contract/IUpdateService.cs b/src/JsonApiDotNetCore/Services/Contract/IUpdateService.cs new file mode 100644 index 0000000000..ca2e171090 --- /dev/null +++ b/src/JsonApiDotNetCore/Services/Contract/IUpdateService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Services +{ + public interface IUpdateService : IUpdateService + where T : class, IIdentifiable + { } + + public interface IUpdateService + where T : class, IIdentifiable + { + Task UpdateAsync(TId id, T entity); + } +} diff --git a/src/JsonApiDotNetCore/Services/EntityResourceService.cs b/src/JsonApiDotNetCore/Services/EntityResourceService.cs index d3c303510b..43a72a1a76 100644 --- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs +++ b/src/JsonApiDotNetCore/Services/EntityResourceService.cs @@ -90,13 +90,13 @@ public async Task GetRelationshipAsync(TId id, string relationshipName) .GetRelationshipName(relationshipName); if (relationshipName == null) - throw new JsonApiException("422", "Relationship name not specified."); + throw new JsonApiException(422, "Relationship name not specified."); _logger.LogTrace($"Looking up '{relationshipName}'..."); var entity = await _entities.GetAndIncludeAsync(id, relationshipName); if (entity == null) - throw new JsonApiException("404", $"Relationship {relationshipName} not found."); + throw new JsonApiException(404, $"Relationship {relationshipName} not found."); var relationship = _jsonApiContext.ContextGraph .GetRelationship(entity, relationshipName); @@ -121,12 +121,12 @@ public async Task UpdateRelationshipsAsync(TId id, string relationshipName, List .GetRelationshipName(relationshipName); if (relationshipName == null) - throw new JsonApiException("422", "Relationship name not specified."); + throw new JsonApiException(422, "Relationship name not specified."); var entity = await _entities.GetAndIncludeAsync(id, relationshipName); if (entity == null) - throw new JsonApiException("404", $"Entity with id {id} could not be found."); + throw new JsonApiException(404, $"Entity with id {id} could not be found."); var relationship = _jsonApiContext.ContextGraph .GetContextEntity(typeof(T)) diff --git a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs index 0319f0aeb6..7b1b9e67a8 100644 --- a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Reflection; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Data; @@ -12,7 +14,7 @@ namespace JsonApiDotNetCore.Services public interface IJsonApiContext { JsonApiOptions Options { get; set; } - IJsonApiContext ApplyContext(); + IJsonApiContext ApplyContext(object controller); IContextGraph ContextGraph { get; set; } ContextEntity RequestEntity { get; set; } string BasePath { get; set; } @@ -25,6 +27,8 @@ public interface IJsonApiContext IGenericProcessorFactory GenericProcessorFactory { get; set; } Dictionary AttributesToUpdate { get; set; } Dictionary RelationshipsToUpdate { get; set; } + Type ControllerType { get; set; } + TAttribute GetControllerAttribute() where TAttribute : Attribute; IDbContextResolver GetDbContextResolver(); } } diff --git a/src/JsonApiDotNetCore/Services/IQueryAccessor.cs b/src/JsonApiDotNetCore/Services/IQueryAccessor.cs new file mode 100644 index 0000000000..51b3ccbbbf --- /dev/null +++ b/src/JsonApiDotNetCore/Services/IQueryAccessor.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace JsonApiDotNetCore.Services +{ + public interface IQueryAccessor + { + bool TryGetValue(string key, out T value); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Services/IResourceService.cs b/src/JsonApiDotNetCore/Services/IResourceService.cs deleted file mode 100644 index ba9a5784ec..0000000000 --- a/src/JsonApiDotNetCore/Services/IResourceService.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCore.Services -{ - public interface IResourceService : IResourceService - where T : class, IIdentifiable - { } - - public interface IResourceService - where T : class, IIdentifiable - { - Task> GetAsync(); - Task GetAsync(TId id); - Task GetRelationshipsAsync(TId id, string relationshipName); - Task GetRelationshipAsync(TId id, string relationshipName); - Task CreateAsync(T entity); - Task UpdateAsync(TId id, T entity); - Task UpdateRelationshipsAsync(TId id, string relationshipName, List relationships); - Task DeleteAsync(TId id); - } -} diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index f4f53c03c1..47702f68f7 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Data; @@ -46,9 +47,15 @@ public JsonApiContext( public IGenericProcessorFactory GenericProcessorFactory { get; set; } public Dictionary AttributesToUpdate { get; set; } = new Dictionary(); public Dictionary RelationshipsToUpdate { get; set; } = new Dictionary(); + public Type ControllerType { get; set; } - public IJsonApiContext ApplyContext() + public IJsonApiContext ApplyContext(object controller) { + if (controller == null) + throw new JsonApiException(500, $"Cannot ApplyContext from null controller for type {typeof(T)}"); + + ControllerType = controller.GetType(); + var context = _httpContextAccessor.HttpContext; var path = context.Request.Path.Value.Split('/'); @@ -83,5 +90,11 @@ private PageManager GetPageManager() PageSize = query.PageSize > 0 ? query.PageSize : Options.DefaultPageSize }; } + + public TAttribute GetControllerAttribute() where TAttribute : Attribute + { + var attribute = ControllerType.GetTypeInfo().GetCustomAttribute(typeof(TAttribute)); + return attribute == null ? null : (TAttribute)attribute; + } } } diff --git a/src/JsonApiDotNetCore/Services/QueryAccessor.cs b/src/JsonApiDotNetCore/Services/QueryAccessor.cs new file mode 100644 index 0000000000..8134028a3e --- /dev/null +++ b/src/JsonApiDotNetCore/Services/QueryAccessor.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Internal; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.Services +{ + public class QueryAccessor : IQueryAccessor + { + private readonly IJsonApiContext _jsonApiContext; + private readonly ILogger _logger; + + public QueryAccessor( + IJsonApiContext jsonApiContext, + ILoggerFactory loggerFactory) + { + _jsonApiContext = jsonApiContext; + _logger = loggerFactory.CreateLogger(); + } + + public bool TryGetValue(string key, out T value) + { + value = default(T); + + var stringValue = GetFilterValue(key); + if(stringValue == null) + { + _logger.LogInformation($"'{key}' was not found in the query collection"); + return false; + } + + try + { + value = TypeHelper.ConvertType(stringValue); + return true; + } + catch (FormatException) + { + _logger.LogInformation($"'{value}' is not a valid guid value for query parameter {key}"); + return false; + } + } + + private string GetFilterValue(string key) => _jsonApiContext.QuerySet + .Filters + .FirstOrDefault(f => f.Key == key) + ?.Value; + } +} \ No newline at end of file diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Repositories/AuthorizedTodoItemsRepository.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Repositories/AuthorizedTodoItemsRepository.cs index b443bb605b..7e9abf3784 100644 --- a/test/JsonApiDotNetCoreExampleTests/Helpers/Repositories/AuthorizedTodoItemsRepository.cs +++ b/test/JsonApiDotNetCoreExampleTests/Helpers/Repositories/AuthorizedTodoItemsRepository.cs @@ -11,16 +11,14 @@ namespace JsonApiDotNetCoreExampleTests.Repositories public class AuthorizedTodoItemsRepository : DefaultEntityRepository { private readonly ILogger _logger; - private readonly AppDbContext _context; private readonly IAuthorizationService _authService; - public AuthorizedTodoItemsRepository(AppDbContext context, + public AuthorizedTodoItemsRepository( ILoggerFactory loggerFactory, IJsonApiContext jsonApiContext, IAuthorizationService authService) - : base(context, loggerFactory, jsonApiContext) + : base(loggerFactory, jsonApiContext) { - _context = context; _logger = loggerFactory.CreateLogger(); _authService = authService; } diff --git a/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj b/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj index 3b0e140c03..84bca08a77 100755 --- a/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj +++ b/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj @@ -19,8 +19,8 @@ - - + + diff --git a/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj b/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj index 7f429caac3..a8a6dd1b50 100644 --- a/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj +++ b/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj @@ -19,9 +19,9 @@ - + - + diff --git a/test/UnitTests/Builders/DocumentBuilder_Tests.cs b/test/UnitTests/Builders/DocumentBuilder_Tests.cs new file mode 100644 index 0000000000..bffbc35d5c --- /dev/null +++ b/test/UnitTests/Builders/DocumentBuilder_Tests.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using Moq; +using Xunit; + +namespace UnitTests +{ + public class DocumentBuilder_Tests + { + private readonly Mock _jsonApiContextMock; + private readonly PageManager _pageManager; + private readonly JsonApiOptions _options; + + public DocumentBuilder_Tests() + { + _jsonApiContextMock = new Mock(); + + _options = new JsonApiOptions(); + + _options.BuildContextGraph(builder => + { + builder.AddResource("models"); + }); + + _jsonApiContextMock + .Setup(m => m.Options) + .Returns(_options); + + _jsonApiContextMock + .Setup(m => m.ContextGraph) + .Returns(_options.ContextGraph); + + _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.ContextGraph.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.BuildContextGraph(builder => + { + builder.DocumentLinks = Link.None; + builder.AddResource("models"); + }); + + _jsonApiContextMock + .Setup(m => m.ContextGraph) + .Returns(_options.ContextGraph); + + 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.ContextGraph) + .Returns(_options.ContextGraph); + + 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); + } + + private class Model : Identifiable + { + [HasOne("related-model", Link.None)] + public RelatedModel RelatedModel { get; set; } + public int RelatedModelId { get; set; } + } + + private class RelatedModel : Identifiable + { + [HasMany("models")] + public List Models { get; set; } + } + } +} diff --git a/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs b/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs new file mode 100644 index 0000000000..9c59372846 --- /dev/null +++ b/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs @@ -0,0 +1,238 @@ +using System; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using Moq; +using Xunit; +using System.Threading.Tasks; +using JsonApiDotNetCore.Internal; + +namespace UnitTests +{ + public class BaseJsonApiController_Tests + { + public class Resource : Identifiable { } + private Mock _jsonApiContextMock = new Mock(); + + [Fact] + public async Task GetAsync_Calls_Service() + { + // arrange + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getAll: serviceMock.Object); + + // act + await controller.GetAsync(); + + // assert + serviceMock.Verify(m => m.GetAsync(), Times.Once); + VerifyApplyContext(); + } + + [Fact] + public async Task GetAsync_Throws_405_If_No_Service() + { + // arrange + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, null); + + // act + var exception = await Assert.ThrowsAsync(() => controller.GetAsync()); + + // assert + Assert.Equal(405, exception.GetStatusCode()); + } + + [Fact] + public async Task GetAsyncById_Calls_Service() + { + // arrange + const int id = 0; + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getById: serviceMock.Object); + + // act + await controller.GetAsync(id); + + // assert + serviceMock.Verify(m => m.GetAsync(id), Times.Once); + VerifyApplyContext(); + } + + [Fact] + 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); + + // act + var exception = await Assert.ThrowsAsync(() => controller.GetAsync(id)); + + // assert + Assert.Equal(405, exception.GetStatusCode()); + } + + [Fact] + public async Task GetRelationshipsAsync_Calls_Service() + { + // arrange + const int id = 0; + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationships: serviceMock.Object); + + // act + await controller.GetRelationshipsAsync(id, string.Empty); + + // assert + serviceMock.Verify(m => m.GetRelationshipsAsync(id, string.Empty), Times.Once); + VerifyApplyContext(); + } + + [Fact] + 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); + + // act + var exception = await Assert.ThrowsAsync(() => controller.GetRelationshipsAsync(id, string.Empty)); + + // assert + Assert.Equal(405, exception.GetStatusCode()); + } + + [Fact] + public async Task GetRelationshipAsync_Calls_Service() + { + // arrange + const int id = 0; + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationship: serviceMock.Object); + + // act + await controller.GetRelationshipAsync(id, string.Empty); + + // assert + serviceMock.Verify(m => m.GetRelationshipAsync(id, string.Empty), Times.Once); + VerifyApplyContext(); + } + + [Fact] + 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); + + // act + var exception = await Assert.ThrowsAsync(() => controller.GetRelationshipAsync(id, string.Empty)); + + // assert + Assert.Equal(405, exception.GetStatusCode()); + } + + [Fact] + public async Task PatchAsync_Calls_Service() + { + // arrange + const int id = 0; + var resource = new Resource(); + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: serviceMock.Object); + + // act + await controller.PatchAsync(id, resource); + + // assert + serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny()), Times.Once); + VerifyApplyContext(); + } + + [Fact] + 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); + + // act + var exception = await Assert.ThrowsAsync(() => controller.PatchAsync(id, It.IsAny())); + + // assert + Assert.Equal(405, exception.GetStatusCode()); + } + + [Fact] + public async Task PatchRelationshipsAsync_Calls_Service() + { + // arrange + const int id = 0; + var resource = new Resource(); + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.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] + 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); + + // act + var exception = await Assert.ThrowsAsync(() => controller.PatchRelationshipsAsync(id, string.Empty, null)); + + // assert + Assert.Equal(405, exception.GetStatusCode()); + } + + [Fact] + public async Task DeleteAsync_Calls_Service() + { + // arrange + const int id = 0; + var resource = new Resource(); + var serviceMock = new Mock>(); + var controller = new BaseJsonApiController(_jsonApiContextMock.Object, delete: serviceMock.Object); + + // act + await controller.DeleteAsync(id); + + // assert + serviceMock.Verify(m => m.DeleteAsync(id), Times.Once); + VerifyApplyContext(); + } + + [Fact] + 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); + + // act + var exception = await Assert.ThrowsAsync(() => controller.DeleteAsync(id)); + + // assert + Assert.Equal(405, exception.GetStatusCode()); + } + + private void VerifyApplyContext() + => _jsonApiContextMock.Verify(m => m.ApplyContext(It.IsAny>()), Times.Once); + } +} diff --git a/test/UnitTests/Controllers/JsonApiControllerMixin_Tests.cs b/test/UnitTests/Controllers/JsonApiControllerMixin_Tests.cs index f201b29fcf..a18aeaf668 100644 --- a/test/UnitTests/Controllers/JsonApiControllerMixin_Tests.cs +++ b/test/UnitTests/Controllers/JsonApiControllerMixin_Tests.cs @@ -38,7 +38,6 @@ public void Errors_Correctly_Infers_Status_Code() new Error("502", "really bad specific"), } }; - // act var result422 = this.Errors(errors422); diff --git a/test/UnitTests/Internal/QuerySet_Tests.cs b/test/UnitTests/Internal/QuerySet_Tests.cs new file mode 100644 index 0000000000..7a78c7ee4a --- /dev/null +++ b/test/UnitTests/Internal/QuerySet_Tests.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Moq; +using Xunit; + +namespace UnitTests.Internal +{ + public class QuerySet_Tests + { + private readonly Mock _jsonApiContextMock; + private readonly Mock _queryCollectionMock; + + public QuerySet_Tests() + { + _jsonApiContextMock = new Mock(); + _queryCollectionMock = new Mock(); + } + + [Fact] + public void Can_Build_Filters() + { + // arrange + var query = new Dictionary { + { "filter[key]", new StringValues("value") } + }; + + _queryCollectionMock + .Setup(m => m.GetEnumerator()) + .Returns(query.GetEnumerator()); + + _jsonApiContextMock + .Setup(m => m.GetControllerAttribute()) + .Returns(new DisableQueryAttribute(QueryParams.None)); + + // act -- ctor calls BuildQuerySet() + var querySet = new QuerySet( + _jsonApiContextMock.Object, + _queryCollectionMock.Object); + + // assert + Assert.Equal("value", querySet.Filters.Single(f => f.Key == "Key").Value); + } + + [Fact] + public void Can_Disable_Filters() + { + // arrange + var query = new Dictionary { + { "filter[key]", new StringValues("value") } + }; + + _queryCollectionMock + .Setup(m => m.GetEnumerator()) + .Returns(query.GetEnumerator()); + + _jsonApiContextMock + .Setup(m => m.GetControllerAttribute()) + .Returns(new DisableQueryAttribute(QueryParams.Filter)); + + // act -- ctor calls BuildQuerySet() + var querySet = new QuerySet( + _jsonApiContextMock.Object, + _queryCollectionMock.Object); + + // assert + Assert.Empty(querySet.Filters); + } + + [Fact] + public void Can_Disable_Sort() + { + // arrange + var query = new Dictionary { + { "sort", new StringValues("-key") } + }; + + _queryCollectionMock + .Setup(m => m.GetEnumerator()) + .Returns(query.GetEnumerator()); + + _jsonApiContextMock + .Setup(m => m.GetControllerAttribute()) + .Returns(new DisableQueryAttribute(QueryParams.Sort)); + + // act -- ctor calls BuildQuerySet() + var querySet = new QuerySet( + _jsonApiContextMock.Object, + _queryCollectionMock.Object); + + // assert + Assert.Empty(querySet.SortParameters); + } + + [Fact] + public void Can_Disable_Include() + { + // arrange + var query = new Dictionary { + { "include", new StringValues("key") } + }; + + _queryCollectionMock + .Setup(m => m.GetEnumerator()) + .Returns(query.GetEnumerator()); + + _jsonApiContextMock + .Setup(m => m.GetControllerAttribute()) + .Returns(new DisableQueryAttribute(QueryParams.Include)); + + // act -- ctor calls BuildQuerySet() + var querySet = new QuerySet( + _jsonApiContextMock.Object, + _queryCollectionMock.Object); + + // assert + Assert.Empty(querySet.IncludedRelationships); + } + + [Fact] + public void Can_Disable_Page() + { + // arrange + var query = new Dictionary { + { "page[size]", new StringValues("1") } + }; + + _queryCollectionMock + .Setup(m => m.GetEnumerator()) + .Returns(query.GetEnumerator()); + + _jsonApiContextMock + .Setup(m => m.GetControllerAttribute()) + .Returns(new DisableQueryAttribute(QueryParams.Page)); + + // act -- ctor calls BuildQuerySet() + var querySet = new QuerySet( + _jsonApiContextMock.Object, + _queryCollectionMock.Object); + + // assert + Assert.Equal(0, querySet.PageQuery.PageSize); + } + + [Fact] + public void Can_Disable_Fields() + { + // arrange + var query = new Dictionary { + { "fields", new StringValues("key") } + }; + + _queryCollectionMock + .Setup(m => m.GetEnumerator()) + .Returns(query.GetEnumerator()); + + _jsonApiContextMock + .Setup(m => m.GetControllerAttribute()) + .Returns(new DisableQueryAttribute(QueryParams.Fields)); + + // act -- ctor calls BuildQuerySet() + var querySet = new QuerySet( + _jsonApiContextMock.Object, + _queryCollectionMock.Object); + + // assert + Assert.Empty(querySet.Fields); + } + } +} diff --git a/test/UnitTests/Internal/TypeHelper_Tests.cs b/test/UnitTests/Internal/TypeHelper_Tests.cs index 1e75e705d2..6fcaa68eb5 100644 --- a/test/UnitTests/Internal/TypeHelper_Tests.cs +++ b/test/UnitTests/Internal/TypeHelper_Tests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using JsonApiDotNetCore.Internal; using Xunit; @@ -44,9 +45,82 @@ public void Can_Convert_Enums() Assert.Equal(TestEnum.Test, result); } - public enum TestEnum + [Fact] + public void ConvertType_Returns_Value_If_Type_Is_Same() + { + // arrange + var val = new ComplexType + { + Property = 1 + }; + + var type = val.GetType(); + + // act + var result = TypeHelper.ConvertType(val, type); + + // assert + Assert.Equal(val, result); + } + + [Fact] + public void ConvertType_Returns_Value_If_Type_Is_Assignable() + { + // arrange + var val = new ComplexType + { + Property = 1 + }; + + var baseType = typeof(BaseType); + var iType = typeof(IType); + + // act + var baseResult = TypeHelper.ConvertType(val, baseType); + var iResult = TypeHelper.ConvertType(val, iType); + + // assert + Assert.Equal(val, baseResult); + Assert.Equal(val, iResult); + } + + [Fact] + public void ConvertType_Returns_Default_Value_For_Empty_Strings() + { + // arrange -- can't use non-constants in [Theory] + var data = new Dictionary + { + { typeof(int), 0 }, + { typeof(short), (short)0 }, + { typeof(long), (long)0 }, + { typeof(string), "" }, + { typeof(Guid), Guid.Empty }, + }; + + foreach (var t in data) + { + // act + var result = TypeHelper.ConvertType(string.Empty, t.Key); + + // assert + Assert.Equal(t.Value, result); + } + } + + private enum TestEnum { Test = 1 } + + private class ComplexType : BaseType + { + public int Property { get; set; } + } + + private class BaseType : IType + { } + + private interface IType + { } } } diff --git a/test/UnitTests/Models/LinkTests.cs b/test/UnitTests/Models/LinkTests.cs new file mode 100644 index 0000000000..e954ddf135 --- /dev/null +++ b/test/UnitTests/Models/LinkTests.cs @@ -0,0 +1,78 @@ +using JsonApiDotNetCore.Models; +using Xunit; + +namespace UnitTests.Models +{ + public class LinkTests + { + [Fact] + public void All_Contains_All_Flags_Except_None() + { + // arrange + var e = Link.All; + + // assert + Assert.True(e.HasFlag(Link.Self)); + Assert.True(e.HasFlag(Link.Paging)); + Assert.True(e.HasFlag(Link.Related)); + Assert.True(e.HasFlag(Link.All)); + Assert.False(e.HasFlag(Link.None)); + } + + [Fact] + public void None_Contains_Only_None() + { + // arrange + var e = Link.None; + + // assert + Assert.False(e.HasFlag(Link.Self)); + Assert.False(e.HasFlag(Link.Paging)); + Assert.False(e.HasFlag(Link.Related)); + Assert.False(e.HasFlag(Link.All)); + Assert.True(e.HasFlag(Link.None)); + } + + [Fact] + public void Self() + { + // arrange + var e = Link.Self; + + // assert + Assert.True(e.HasFlag(Link.Self)); + Assert.False(e.HasFlag(Link.Paging)); + Assert.False(e.HasFlag(Link.Related)); + Assert.False(e.HasFlag(Link.All)); + Assert.False(e.HasFlag(Link.None)); + } + + [Fact] + public void Paging() + { + // arrange + var e = Link.Paging; + + // assert + Assert.False(e.HasFlag(Link.Self)); + Assert.True(e.HasFlag(Link.Paging)); + Assert.False(e.HasFlag(Link.Related)); + Assert.False(e.HasFlag(Link.All)); + Assert.False(e.HasFlag(Link.None)); + } + + [Fact] + public void Related() + { + // arrange + var e = Link.Related; + + // assert + Assert.False(e.HasFlag(Link.Self)); + Assert.False(e.HasFlag(Link.Paging)); + Assert.True(e.HasFlag(Link.Related)); + Assert.False(e.HasFlag(Link.All)); + Assert.False(e.HasFlag(Link.None)); + } + } +} diff --git a/test/UnitTests/Serialization/DasherizedResolverTests.cs b/test/UnitTests/Serialization/DasherizedResolverTests.cs new file mode 100644 index 0000000000..5c0c4d08f3 --- /dev/null +++ b/test/UnitTests/Serialization/DasherizedResolverTests.cs @@ -0,0 +1,28 @@ +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/JsonApiDeSerializerTests.cs b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs new file mode 100644 index 0000000000..53edc9faad --- /dev/null +++ b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs @@ -0,0 +1,222 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal.Generics; +using JsonApiDotNetCore.Models; +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 contextGraphBuilder = new ContextGraphBuilder(); + contextGraphBuilder.AddResource("test-resource"); + var contextGraph = contextGraphBuilder.Build(); + + var jsonApiContextMock = new Mock(); + jsonApiContextMock.SetupAllProperties(); + jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); + jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); + jsonApiContextMock.Setup(m => m.Options).Returns(new JsonApiOptions + { + JsonContractResolver = new CamelCasePropertyNamesContractResolver() + }); + + var genericProcessorFactoryMock = new Mock(); + + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object); + + var content = new Document + { + Data = new DocumentData + { + 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 contextGraphBuilder = new ContextGraphBuilder(); + contextGraphBuilder.AddResource("test-resource"); + var contextGraph = contextGraphBuilder.Build(); + + var jsonApiContextMock = new Mock(); + jsonApiContextMock.SetupAllProperties(); + jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); + jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); + jsonApiContextMock.Setup(m => m.Options).Returns(new JsonApiOptions + { + JsonContractResolver = new CamelCasePropertyNamesContractResolver() + }); + + var genericProcessorFactoryMock = new Mock(); + + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object); + + var content = new Document + { + Data = new DocumentData + { + 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 contextGraphBuilder = new ContextGraphBuilder(); + contextGraphBuilder.AddResource("test-resource"); + var contextGraph = contextGraphBuilder.Build(); + + var jsonApiContextMock = new Mock(); + jsonApiContextMock.SetupAllProperties(); + jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); + jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); + + jsonApiContextMock.Setup(m => m.Options).Returns(new JsonApiOptions + { + JsonContractResolver = new DasherizedResolver() // <--- + }); + + var genericProcessorFactoryMock = new Mock(); + + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object); + + var content = new Document + { + Data = new DocumentData + { + 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 contextGraphBuilder = new ContextGraphBuilder(); + contextGraphBuilder.AddResource("test-resource"); + var contextGraph = contextGraphBuilder.Build(); + + var attributesToUpdate = new Dictionary(); + + var jsonApiContextMock = new Mock(); + jsonApiContextMock.SetupAllProperties(); + jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); + jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(attributesToUpdate); + + jsonApiContextMock.Setup(m => m.Options).Returns(new JsonApiOptions + { + JsonContractResolver = new DasherizedResolver() + }); + + var genericProcessorFactoryMock = new Mock(); + + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object); + + var content = new Document + { + Data = new DocumentData + { + 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.Equal(1, attributesToUpdate.Count); + + foreach(var attr in attributesToUpdate) + Assert.False(attr.Key.IsImmutable); + } + + 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; } + } + } +} diff --git a/test/UnitTests/Serialization/JsonApiSerializerTests.cs b/test/UnitTests/Serialization/JsonApiSerializerTests.cs new file mode 100644 index 0000000000..e671f3fc0c --- /dev/null +++ b/test/UnitTests/Serialization/JsonApiSerializerTests.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Generics; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Services; +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using Xunit; + +namespace UnitTests.Serialization +{ + public class JsonApiSerializerTests + { + [Fact] + public void Can_Serialize_Complex_Types() + { + // arrange + var contextGraphBuilder = new ContextGraphBuilder(); + contextGraphBuilder.AddResource("test-resource"); + var contextGraph = contextGraphBuilder.Build(); + + var jsonApiContextMock = new Mock(); + jsonApiContextMock.SetupAllProperties(); + jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); + jsonApiContextMock.Setup(m => m.Options).Returns(new JsonApiOptions { + JsonContractResolver = new DasherizedResolver() + }); + jsonApiContextMock.Setup(m => m.RequestEntity) + .Returns(contextGraph.GetContextEntity("test-resource")); + jsonApiContextMock.Setup(m => m.MetaBuilder).Returns(new MetaBuilder()); + jsonApiContextMock.Setup(m => m.PageManager).Returns(new PageManager()); + + var documentBuilder = new DocumentBuilder(jsonApiContextMock.Object); + var serializer = new JsonApiSerializer(jsonApiContextMock.Object, documentBuilder); + var resource = new TestResource { + ComplexMember = new ComplexType { + CompoundName = "testname" + } + }; + + // act + var result = serializer.Serialize(resource); + + // assert + Assert.NotNull(result); + Assert.Equal("{\"data\":{\"type\":\"test-resource\",\"id\":\"\",\"attributes\":{\"complex-member\":{\"compound-name\":\"testname\"}}}}", result); + } + + private class TestResource : Identifiable + { + [Attr("complex-member")] + public ComplexType ComplexMember { get; set; } + } + + private class ComplexType + { + public string CompoundName { get; set; } + } + } +} diff --git a/test/UnitTests/Services/QueryAccessorTests.cs b/test/UnitTests/Services/QueryAccessorTests.cs new file mode 100644 index 0000000000..df455b1b92 --- /dev/null +++ b/test/UnitTests/Services/QueryAccessorTests.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; +using Moq; +using Xunit; + +namespace UnitTests.Services +{ + public class QueryAccessorTests + { + private readonly Mock _contextMock; + private readonly Mock _loggerMock; + private readonly Mock _queryMock; + + public QueryAccessorTests() + { + _contextMock = new Mock(); + _loggerMock = new Mock(); + _queryMock = new Mock(); + } + + [Fact] + public void Can_Get_Guid_QueryValue() + { + // arrange + const string key = "some-id"; + var filterQuery = $"filter[{key}]"; + var value = Guid.NewGuid(); + + var query = new Dictionary { + { filterQuery, value.ToString() } + }; + + _queryMock.Setup(q => q.GetEnumerator()).Returns(query.GetEnumerator()); + + var querySet = new QuerySet(_contextMock.Object, _queryMock.Object); + _contextMock.Setup(c => c.QuerySet) + .Returns(querySet); + + var service = new QueryAccessor(_contextMock.Object, _loggerMock.Object); + + // act + var success = service.TryGetValue("SomeId", out Guid result); + + // assert + Assert.True(success); + Assert.Equal(value, result); + } + } +} diff --git a/test/UnitTests/UnitTests.csproj b/test/UnitTests/UnitTests.csproj index 1af9a3a29f..d29e136490 100644 --- a/test/UnitTests/UnitTests.csproj +++ b/test/UnitTests/UnitTests.csproj @@ -11,6 +11,6 @@ - + \ No newline at end of file