diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000..134066baff
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,14 @@
+# EditorConfig is awesome: http://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+charset = utf-8
+
+[*.{csproj,props}]
+indent_size = 2
diff --git a/JsonApiDotnetCore.sln b/JsonApiDotnetCore.sln
index a144223671..385fa4d6ad 100644
--- a/JsonApiDotnetCore.sln
+++ b/JsonApiDotnetCore.sln
@@ -28,6 +28,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{02
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReportsExample", "src\Examples\ReportsExample\ReportsExample.csproj", "{FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{076E1AE4-FD25-4684-B826-CAAE37FEA0AA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "benchmarks\Benchmarks.csproj", "{1F604666-BB0F-413E-922D-9D37C6073285}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -106,10 +110,22 @@ Global
{FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Debug|x86.Build.0 = Debug|Any CPU
{FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|Any CPU.Build.0 = Release|Any CPU
- {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|x64.ActiveCfg = Release|Any CPU
- {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|x64.Build.0 = Release|Any CPU
- {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|x86.ActiveCfg = Release|Any CPU
- {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|x86.Build.0 = Release|Any CPU
+ {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
+ {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|x64.ActiveCfg = Debug|x64
+ {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|x64.Build.0 = Debug|x64
+ {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|x86.ActiveCfg = Debug|x86
+ {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|x86.Build.0 = Debug|x86
+ {1F604666-BB0F-413E-922D-9D37C6073285}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1F604666-BB0F-413E-922D-9D37C6073285}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1F604666-BB0F-413E-922D-9D37C6073285}.Release|x64.ActiveCfg = Release|x64
+ {1F604666-BB0F-413E-922D-9D37C6073285}.Release|x64.Build.0 = Release|x64
+ {1F604666-BB0F-413E-922D-9D37C6073285}.Release|x86.ActiveCfg = Release|x86
+ {1F604666-BB0F-413E-922D-9D37C6073285}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -123,6 +139,7 @@ Global
{6D4BD85A-A262-44C6-8572-FE3A30410BF3} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
{026FBC6C-AF76-4568-9B87-EC73457899FD} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF}
{FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D} = {026FBC6C-AF76-4568-9B87-EC73457899FD}
+ {1F604666-BB0F-413E-922D-9D37C6073285} = {076E1AE4-FD25-4684-B826-CAAE37FEA0AA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A2421882-8F0A-4905-928F-B550B192F9A4}
diff --git a/benchmarks/.gitignore b/benchmarks/.gitignore
new file mode 100644
index 0000000000..5a3c72cbbb
--- /dev/null
+++ b/benchmarks/.gitignore
@@ -0,0 +1,239 @@
+_data/
+*-report-default.md
+*-report.csv
+*-report.html
+
+## 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/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.Query.QueryParser_Benchmarks-report-github.md b/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.Query.QueryParser_Benchmarks-report-github.md
new file mode 100755
index 0000000000..b3ebd8a29c
--- /dev/null
+++ b/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.Query.QueryParser_Benchmarks-report-github.md
@@ -0,0 +1,16 @@
+``` ini
+
+BenchmarkDotNet=v0.10.10, OS=Mac OS X 10.12
+Processor=Intel Core i5-5257U CPU 2.70GHz (Broadwell), ProcessorCount=4
+.NET Core SDK=2.0.0
+ [Host] : .NET Core 1.1.4 (Framework 4.6.25714.03), 64bit RyuJIT
+ Job-WKDOLS : .NET Core 1.1.4 (Framework 4.6.25714.03), 64bit RyuJIT
+
+LaunchCount=3 TargetCount=20 WarmupCount=10
+
+```
+| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Allocated |
+|--------------- |-------------:|-----------:|-----------:|---------:|--------:|----------:|
+| AscendingSort | 4.316 us | 1.3773 us | 3.0232 us | 0.5066 | 0.1303 | 1.08 KB |
+| DescendingSort | 3.300 us | 0.0314 us | 0.0682 us | 0.5123 | 0.1318 | 1.13 KB |
+| ComplexQuery | 2,041.642 us | 41.5631 us | 92.1010 us | 312.5000 | 80.2734 | 648.99 KB |
diff --git a/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.Serialization.JsonApiDeserializer_Benchmarks-report-github.md b/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.Serialization.JsonApiDeserializer_Benchmarks-report-github.md
new file mode 100755
index 0000000000..6c8c3f2905
--- /dev/null
+++ b/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.Serialization.JsonApiDeserializer_Benchmarks-report-github.md
@@ -0,0 +1,13 @@
+``` ini
+
+BenchmarkDotNet=v0.10.10, OS=Mac OS X 10.12
+Processor=Intel Core i5-5257U CPU 2.70GHz (Broadwell), ProcessorCount=4
+.NET Core SDK=2.0.0
+ [Host] : .NET Core 1.1.4 (Framework 4.6.25714.03), 64bit RyuJIT
+ DefaultJob : .NET Core 1.1.4 (Framework 4.6.25714.03), 64bit RyuJIT
+
+
+```
+| Method | Mean | Error | StdDev |
+|------------------------ |---------:|----------:|----------:|
+| DeserializeSimpleObject | 27.05 us | 0.5353 us | 0.5950 us |
diff --git a/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.Serialization.JsonApiSerializer_Benchmarks-report-github.md b/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.Serialization.JsonApiSerializer_Benchmarks-report-github.md
new file mode 100755
index 0000000000..f86bf0faa9
--- /dev/null
+++ b/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.Serialization.JsonApiSerializer_Benchmarks-report-github.md
@@ -0,0 +1,13 @@
+``` ini
+
+BenchmarkDotNet=v0.10.10, OS=Mac OS X 10.12
+Processor=Intel Core i5-5257U CPU 2.70GHz (Broadwell), ProcessorCount=4
+.NET Core SDK=2.0.0
+ [Host] : .NET Core 1.1.4 (Framework 4.6.25714.03), 64bit RyuJIT
+ DefaultJob : .NET Core 1.1.4 (Framework 4.6.25714.03), 64bit RyuJIT
+
+
+```
+| Method | Mean | Error | StdDev |
+|---------------------- |---------:|----------:|----------:|
+| SerializeSimpleObject | 7.195 us | 0.1436 us | 0.1816 us |
diff --git a/benchmarks/Benchmarks.csproj b/benchmarks/Benchmarks.csproj
new file mode 100644
index 0000000000..b5ff121826
--- /dev/null
+++ b/benchmarks/Benchmarks.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ $(NetCoreAppVersion)
+ Benchmarks
+
+
+
+
+
+
+
+
+
+
diff --git a/benchmarks/Program.cs b/benchmarks/Program.cs
new file mode 100644
index 0000000000..7665d5fb97
--- /dev/null
+++ b/benchmarks/Program.cs
@@ -0,0 +1,16 @@
+using BenchmarkDotNet.Running;
+using Benchmarks.Query;
+using Benchmarks.Serialization;
+
+namespace Benchmarks {
+ class Program {
+ static void Main(string[] args) {
+ var switcher = new BenchmarkSwitcher(new[] {
+ typeof(JsonApiDeserializer_Benchmarks),
+ typeof(JsonApiSerializer_Benchmarks),
+ typeof(QueryParser_Benchmarks)
+ });
+ switcher.Run(args);
+ }
+ }
+}
diff --git a/benchmarks/Query/QueryParser_Benchmarks.cs b/benchmarks/Query/QueryParser_Benchmarks.cs
new file mode 100644
index 0000000000..61fe3b1bc2
--- /dev/null
+++ b/benchmarks/Query/QueryParser_Benchmarks.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Attributes.Exporters;
+using BenchmarkDotNet.Attributes.Jobs;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Internal;
+using JsonApiDotNetCore.Models;
+using JsonApiDotNetCore.Services;
+using Microsoft.AspNetCore.Http.Internal;
+using Microsoft.Extensions.Primitives;
+using Moq;
+
+namespace Benchmarks.Query {
+ [MarkdownExporter, SimpleJob(launchCount : 3, warmupCount : 10, targetCount : 20), MemoryDiagnoser]
+ public class QueryParser_Benchmarks {
+ private readonly BenchmarkFacade _queryParser;
+
+ private const string ATTRIBUTE = "Attribute";
+ private const string ASCENDING_SORT = ATTRIBUTE;
+ private const string DESCENDING_SORT = "-" + ATTRIBUTE;
+
+ public QueryParser_Benchmarks() {
+ var controllerContextMock = new Mock();
+ controllerContextMock.Setup(m => m.RequestEntity).Returns(new ContextEntity {
+ Attributes = new List {
+ new AttrAttribute(ATTRIBUTE) {
+ InternalAttributeName = ATTRIBUTE
+ }
+ }
+ });
+ var options = new JsonApiOptions();
+ _queryParser = new BenchmarkFacade(controllerContextMock.Object, options);
+ }
+
+ [Benchmark]
+ public void AscendingSort() => _queryParser._ParseSortParameters(ASCENDING_SORT);
+
+ [Benchmark]
+ public void DescendingSort() => _queryParser._ParseSortParameters(DESCENDING_SORT);
+
+ [Benchmark]
+ public void ComplexQuery() => Run(100, () => _queryParser.Parse(
+ new QueryCollection(
+ new Dictionary {
+ { $"filter[{ATTRIBUTE}]", new StringValues(new [] { "abc", "eq:abc" }) },
+ { $"sort", $"-{ATTRIBUTE}" },
+ { $"include", "relationship" },
+ { $"page[size]", "1" },
+ { $"fields[resource]", ATTRIBUTE },
+ }
+ )
+ ));
+
+ private void Run(int iterations, Action action) {
+ for (int i = 0; i < iterations; i++)
+ action();
+ }
+
+ // this facade allows us to expose and micro-benchmark protected methods
+ private class BenchmarkFacade : QueryParser {
+ public BenchmarkFacade(
+ IControllerContext controllerContext,
+ JsonApiOptions options) : base(controllerContext, options) { }
+
+ public void _ParseSortParameters(string value) => base.ParseSortParameters(value);
+ }
+ }
+}
diff --git a/benchmarks/Serialization/JsonApiDeserializer_Benchmarks.cs b/benchmarks/Serialization/JsonApiDeserializer_Benchmarks.cs
new file mode 100644
index 0000000000..c490bee362
--- /dev/null
+++ b/benchmarks/Serialization/JsonApiDeserializer_Benchmarks.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Attributes.Exporters;
+using JsonApiDotNetCore.Builders;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Internal.Generics;
+using JsonApiDotNetCore.Models;
+using JsonApiDotNetCore.Serialization;
+using JsonApiDotNetCore.Services;
+using Moq;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+
+namespace Benchmarks.Serialization {
+ [MarkdownExporter]
+ public class JsonApiDeserializer_Benchmarks {
+ private const string TYPE_NAME = "simple-types";
+ private static readonly string Content = JsonConvert.SerializeObject(new Document {
+ Data = new DocumentData {
+ Type = TYPE_NAME,
+ Id = "1",
+ Attributes = new Dictionary {
+ {
+ "name",
+ Guid.NewGuid().ToString()
+ }
+ }
+ }
+ });
+
+ private readonly JsonApiDeSerializer _jsonApiDeSerializer;
+
+ public JsonApiDeserializer_Benchmarks() {
+ var contextGraphBuilder = new ContextGraphBuilder();
+ contextGraphBuilder.AddResource(TYPE_NAME);
+ 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());
+
+ var jsonApiOptions = new JsonApiOptions();
+ jsonApiOptions.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
+ jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions);
+
+ var genericProcessorFactoryMock = new Mock();
+
+ _jsonApiDeSerializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object);
+ }
+
+ [Benchmark]
+ public object DeserializeSimpleObject() => _jsonApiDeSerializer.Deserialize(Content);
+
+ private class SimpleType : Identifiable {
+ [Attr("name")]
+ public string Name { get; set; }
+ }
+ }
+}
diff --git a/benchmarks/Serialization/JsonApiSerializer_Benchmarks.cs b/benchmarks/Serialization/JsonApiSerializer_Benchmarks.cs
new file mode 100644
index 0000000000..3d5ef7c001
--- /dev/null
+++ b/benchmarks/Serialization/JsonApiSerializer_Benchmarks.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Attributes.Exporters;
+using JsonApiDotNetCore.Builders;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Internal.Generics;
+using JsonApiDotNetCore.Models;
+using JsonApiDotNetCore.Serialization;
+using JsonApiDotNetCore.Services;
+using Moq;
+using Newtonsoft.Json.Serialization;
+
+namespace Benchmarks.Serialization {
+ [MarkdownExporter]
+ public class JsonApiSerializer_Benchmarks {
+ private const string TYPE_NAME = "simple-types";
+ private static readonly SimpleType Content = new SimpleType();
+
+ private readonly JsonApiSerializer _jsonApiSerializer;
+
+ public JsonApiSerializer_Benchmarks() {
+ var contextGraphBuilder = new ContextGraphBuilder();
+ contextGraphBuilder.AddResource(TYPE_NAME);
+ 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());
+
+ var jsonApiOptions = new JsonApiOptions();
+ jsonApiOptions.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
+ jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions);
+
+ var genericProcessorFactoryMock = new Mock();
+
+ var documentBuilder = new DocumentBuilder(jsonApiContextMock.Object);
+ _jsonApiSerializer = new JsonApiSerializer(jsonApiContextMock.Object, documentBuilder);
+ }
+
+ [Benchmark]
+ public object SerializeSimpleObject() => _jsonApiSerializer.Serialize(Content);
+
+ private class SimpleType : Identifiable {
+ [Attr("name")]
+ public string Name { get; set; }
+ }
+ }
+}
diff --git a/build/dependencies.props b/build/dependencies.props
new file mode 100644
index 0000000000..13b9ba4ecb
--- /dev/null
+++ b/build/dependencies.props
@@ -0,0 +1,11 @@
+
+
+ netcoreapp2.0
+ netstandard2.0
+
+
+ 4.7.10
+ 2.2.0
+ 8.0.1-beta-1
+
+
diff --git a/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj b/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj
index dcbf2a2841..a2981cffd1 100755
--- a/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj
+++ b/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj
@@ -1,5 +1,5 @@
-
+
$(NetCoreAppVersion)
true
diff --git a/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj b/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj
index afaf0e7cff..32506808fe 100755
--- a/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj
+++ b/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj
@@ -1,5 +1,5 @@
-
+
$(NetCoreAppVersion)
diff --git a/src/Examples/ReportsExample/ReportsExample.csproj b/src/Examples/ReportsExample/ReportsExample.csproj
index 2131c545a4..f8f83e454c 100644
--- a/src/Examples/ReportsExample/ReportsExample.csproj
+++ b/src/Examples/ReportsExample/ReportsExample.csproj
@@ -1,24 +1,24 @@
-
-
-
- $(NetCoreAppVersion)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ $(NetCoreAppVersion)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiInputFormatter.cs b/src/JsonApiDotNetCore/Formatters/JsonApiInputFormatter.cs
index 12e57deadf..f556b7433d 100644
--- a/src/JsonApiDotNetCore/Formatters/JsonApiInputFormatter.cs
+++ b/src/JsonApiDotNetCore/Formatters/JsonApiInputFormatter.cs
@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
+using JsonApiDotNetCore.Internal;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.DependencyInjection;
@@ -14,7 +15,7 @@ public bool CanRead(InputFormatterContext context)
var contentTypeString = context.HttpContext.Request.ContentType;
- return contentTypeString == "application/vnd.api+json";
+ return contentTypeString == Constants.ContentType;
}
public async Task ReadAsync(InputFormatterContext context)
diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs
index 2431055d1d..b456932fc5 100644
--- a/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs
+++ b/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs
@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
+using JsonApiDotNetCore.Internal;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.DependencyInjection;
@@ -14,7 +15,7 @@ public bool CanWriteResult(OutputFormatterCanWriteContext context)
var contentTypeString = context.HttpContext.Request.ContentType;
- return string.IsNullOrEmpty(contentTypeString) || contentTypeString == "application/vnd.api+json";
+ return string.IsNullOrEmpty(contentTypeString) || contentTypeString == Constants.ContentType;
}
public async Task WriteAsync(OutputFormatterWriteContext context)
diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs b/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs
index 730a88f13e..d644def66d 100644
--- a/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs
+++ b/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs
@@ -13,7 +13,8 @@ public class JsonApiWriter : IJsonApiWriter
private readonly ILogger _logger;
private readonly IJsonApiSerializer _serializer;
- public JsonApiWriter(IJsonApiSerializer serializer,
+ public JsonApiWriter(
+ IJsonApiSerializer serializer,
ILoggerFactory loggerFactory)
{
_serializer = serializer;
@@ -30,7 +31,7 @@ public async Task WriteAsync(OutputFormatterWriteContext context)
var response = context.HttpContext.Response;
using (var writer = context.WriterFactory(response.Body, Encoding.UTF8))
{
- response.ContentType = "application/vnd.api+json";
+ response.ContentType = Constants.ContentType;
string responseContent;
try
{
@@ -55,4 +56,4 @@ private string GetResponseBody(object responseObject)
return _serializer.Serialize(responseObject);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/JsonApiDotNetCore/Internal/Constants.cs b/src/JsonApiDotNetCore/Internal/Constants.cs
new file mode 100644
index 0000000000..750d94ba07
--- /dev/null
+++ b/src/JsonApiDotNetCore/Internal/Constants.cs
@@ -0,0 +1,8 @@
+namespace JsonApiDotNetCore.Internal
+{
+ public static class Constants
+ {
+ public const string AcceptHeader = "Accept";
+ public const string ContentType = "application/vnd.api+json";
+ }
+}
diff --git a/src/JsonApiDotNetCore/Internal/ContextGraph.cs b/src/JsonApiDotNetCore/Internal/ContextGraph.cs
index fd29794194..163196341b 100644
--- a/src/JsonApiDotNetCore/Internal/ContextGraph.cs
+++ b/src/JsonApiDotNetCore/Internal/ContextGraph.cs
@@ -1,7 +1,7 @@
-using System.Reflection;
+using System;
using System.Collections.Generic;
using System.Linq;
-using System;
+using System.Reflection;
namespace JsonApiDotNetCore.Internal
{
@@ -34,9 +34,9 @@ public string GetRelationshipName(string relationshipName)
{
var entityType = typeof(TParent);
return Entities
- .SingleOrDefault(e => e.EntityType == entityType)
- .Relationships
- .SingleOrDefault(r => string.Equals(r.PublicRelationshipName, relationshipName, StringComparison.OrdinalIgnoreCase))
+ .SingleOrDefault(e => e.EntityType == entityType)
+ ?.Relationships
+ .SingleOrDefault(r => string.Equals(r.PublicRelationshipName, relationshipName, StringComparison.OrdinalIgnoreCase))
?.InternalRelationshipName;
}
}
diff --git a/src/JsonApiDotNetCore/Internal/JsonApiExceptionFactory.cs b/src/JsonApiDotNetCore/Internal/JsonApiExceptionFactory.cs
index 42f3037f89..36b4969b1d 100644
--- a/src/JsonApiDotNetCore/Internal/JsonApiExceptionFactory.cs
+++ b/src/JsonApiDotNetCore/Internal/JsonApiExceptionFactory.cs
@@ -5,14 +5,17 @@ namespace JsonApiDotNetCore.Internal
{
public static class JsonApiExceptionFactory
{
+ private const string JsonApiException = nameof(JsonApiException);
+ private const string InvalidCastException = nameof(InvalidCastException);
+
public static JsonApiException GetException(Exception exception)
{
var exceptionType = exception.GetType().ToString().Split('.').Last();
switch(exceptionType)
{
- case "JsonApiException":
+ case JsonApiException:
return (JsonApiException)exception;
- case "InvalidCastException":
+ case InvalidCastException:
return new JsonApiException(409, exception.Message);
default:
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 8af2fe95e1..74db2b342e 100644
--- a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs
+++ b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs
@@ -1,3 +1,4 @@
+using System;
using System.Linq;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Services;
@@ -9,14 +10,14 @@ public class AttrFilterQuery : BaseFilterQuery
private readonly IJsonApiContext _jsonApiContext;
public AttrFilterQuery(
- IJsonApiContext jsonApiCopntext,
+ IJsonApiContext jsonApiContext,
FilterQuery filterQuery)
{
- _jsonApiContext = jsonApiCopntext;
+ _jsonApiContext = jsonApiContext;
- var attribute = GetAttribute(filterQuery.Key);
+ var attribute = GetAttribute(filterQuery.Attribute);
- FilteredAttribute = attribute ?? throw new JsonApiException(400, $"{filterQuery.Key} is not a valid property.");
+ FilteredAttribute = attribute ?? throw new JsonApiException(400, $"'{filterQuery.Attribute}' is not a valid attribute.");
PropertyValue = filterQuery.Value;
FilterOperation = GetFilterOperation(filterQuery.Operation);
}
@@ -25,12 +26,9 @@ public AttrFilterQuery(
public string PropertyValue { get; set; }
public FilterOperations FilterOperation { get; set; }
- private AttrAttribute GetAttribute(string propertyName)
- {
- return _jsonApiContext.RequestEntity.Attributes
- .FirstOrDefault(attr =>
- attr.InternalAttributeName.ToLower() == propertyName.ToLower()
+ private AttrAttribute GetAttribute(string attribute) =>
+ _jsonApiContext.RequestEntity.Attributes.FirstOrDefault(
+ attr => string.Equals(attr.PublicAttributeName, attribute, StringComparison.OrdinalIgnoreCase)
);
- }
}
-}
\ No newline at end of file
+}
diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs
index 527b842ca8..1c43d84254 100644
--- a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs
+++ b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs
@@ -8,10 +8,10 @@ protected FilterOperations GetFilterOperation(string prefix)
{
if (prefix.Length == 0) return FilterOperations.eq;
- if (!Enum.TryParse(prefix, out FilterOperations opertion))
+ if (Enum.TryParse(prefix, out FilterOperations opertion) == false)
throw new JsonApiException(400, $"Invalid filter prefix '{prefix}'");
return opertion;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs
index 7f4f1a40c6..dd72e827ff 100644
--- a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs
+++ b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs
@@ -1,17 +1,23 @@
+using System;
+using JsonApiDotNetCore.Extensions;
+
namespace JsonApiDotNetCore.Internal.Query
{
public class FilterQuery
{
- public FilterQuery(string key, string value, string operation)
+ public FilterQuery(string attribute, string value, string operation)
{
- Key = key;
+ Attribute = attribute;
+ Key = attribute.ToProperCase();
Value = value;
Operation = operation;
}
+ [Obsolete("Key has been replaced by '" + nameof(Attribute) + "'. Members should be located by their public name, not by coercing the provided value to the internal name.")]
public string Key { get; set; }
+ public string Attribute { get; }
public string Value { get; set; }
public string Operation { get; set; }
- public bool IsAttributeOfRelationship => Key.Contains(".");
+ public bool IsAttributeOfRelationship => Attribute.Contains(".");
}
-}
\ No newline at end of file
+}
diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs
index dc633c5302..3b249c176c 100644
--- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs
+++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs
@@ -1,3 +1,4 @@
+using System;
using System.Linq;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Services;
@@ -14,7 +15,7 @@ public RelatedAttrFilterQuery(
{
_jsonApiContext = jsonApiCopntext;
- var relationshipArray = filterQuery.Key.Split('.');
+ var relationshipArray = filterQuery.Attribute.Split('.');
var relationship = GetRelationship(relationshipArray[0]);
if (relationship == null)
@@ -36,14 +37,14 @@ public RelatedAttrFilterQuery(
private RelationshipAttribute GetRelationship(string propertyName)
{
return _jsonApiContext.RequestEntity.Relationships
- .FirstOrDefault(r => r.InternalRelationshipName.ToLower() == propertyName.ToLower());
+ .FirstOrDefault(r => string.Equals(r.PublicRelationshipName, propertyName, StringComparison.OrdinalIgnoreCase));
}
private AttrAttribute GetAttribute(RelationshipAttribute relationship, string attribute)
{
var relatedContextExntity = _jsonApiContext.ContextGraph.GetContextEntity(relationship.Type);
return relatedContextExntity.Attributes
- .FirstOrDefault(a => a.InternalAttributeName.ToLower() == attribute.ToLower());
+ .FirstOrDefault(a => string.Equals(a.PublicAttributeName, attribute, StringComparison.OrdinalIgnoreCase));
}
}
-}
\ No newline at end of file
+}
diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
index 9ebddb4512..53c81b2ec1 100755
--- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
+++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
@@ -1,4 +1,5 @@
+
2.2.0
$(NetStandardVersion)
@@ -20,4 +21,5 @@
+
diff --git a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs
index 670def21d6..6e2612c9a6 100644
--- a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs
+++ b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
+using JsonApiDotNetCore.Internal;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
@@ -37,7 +38,7 @@ private static bool IsValidContentTypeHeader(HttpContext context)
private static bool IsValidAcceptHeader(HttpContext context)
{
- if (context.Request.Headers.TryGetValue("Accept", out StringValues acceptHeaders) == false)
+ if (context.Request.Headers.TryGetValue(Constants.AcceptHeader, out StringValues acceptHeaders) == false)
return true;
foreach (var acceptHeader in acceptHeaders)
@@ -54,7 +55,7 @@ private static bool IsValidAcceptHeader(HttpContext context)
private static bool ContainsMediaTypeParameters(string mediaType)
{
var mediaTypeArr = mediaType.Split(';');
- return (mediaTypeArr[0] == "application/vnd.api+json" && mediaTypeArr.Length == 2);
+ return (mediaTypeArr[0] == Constants.ContentType && mediaTypeArr.Length == 2);
}
private static void FlushResponse(HttpContext context, int statusCode)
diff --git a/src/JsonApiDotNetCore/Models/DocumentBase.cs b/src/JsonApiDotNetCore/Models/DocumentBase.cs
index 1cb31595ec..df51301c20 100644
--- a/src/JsonApiDotNetCore/Models/DocumentBase.cs
+++ b/src/JsonApiDotNetCore/Models/DocumentBase.cs
@@ -15,19 +15,8 @@ public class DocumentBase
public Dictionary Meta { get; set; }
// http://www.newtonsoft.com/json/help/html/ConditionalProperties.htm
- public bool ShouldSerializeIncluded()
- {
- return (Included != null);
- }
-
- public bool ShouldSerializeMeta()
- {
- return (Meta != null);
- }
-
- public bool ShouldSerializeLinks()
- {
- return (Links != null);
- }
+ public bool ShouldSerializeIncluded() => (Included != null);
+ public bool ShouldSerializeMeta() => (Meta != null);
+ public bool ShouldSerializeLinks() => (Links != null);
}
}
diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs
index 25021c441e..1c0c5014f7 100644
--- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs
+++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs
@@ -169,9 +169,12 @@ private object SetHasOneRelationship(object entity,
if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData))
{
var relationshipAttr = _jsonApiContext.RequestEntity.Relationships
- .SingleOrDefault(r => r.PublicRelationshipName == relationshipName);
+ .SingleOrDefault(r => r.PublicRelationshipName == relationshipName);
- var data = (Dictionary)relationshipData.ExposedData;
+ if (relationshipAttr == null)
+ throw new JsonApiException(400, $"{_jsonApiContext.RequestEntity.EntityName} does not contain a relationship '{relationshipName}'");
+
+ var data = (Dictionary) relationshipData.ExposedData;
if (data == null) return entity;
diff --git a/src/JsonApiDotNetCore/Services/QueryAccessor.cs b/src/JsonApiDotNetCore/Services/QueryAccessor.cs
index 09942d4031..41bc64151b 100644
--- a/src/JsonApiDotNetCore/Services/QueryAccessor.cs
+++ b/src/JsonApiDotNetCore/Services/QueryAccessor.cs
@@ -61,9 +61,22 @@ public bool TryGetValue(string key, out T value)
}
}
- private string GetFilterValue(string key) => _jsonApiContext.QuerySet
- .Filters
- .FirstOrDefault(f => string.Equals(f.Key, key, StringComparison.OrdinalIgnoreCase))
- ?.Value;
+ private string GetFilterValue(string key) {
+ var publicValue = _jsonApiContext.QuerySet.Filters
+ .FirstOrDefault(f => string.Equals(f.Attribute, key, StringComparison.OrdinalIgnoreCase))?.Value;
+
+ if(publicValue != null)
+ return publicValue;
+
+ var internalValue = _jsonApiContext.QuerySet.Filters
+ .FirstOrDefault(f => string.Equals(f.Attribute, key, StringComparison.OrdinalIgnoreCase))?.Value;
+
+ if(internalValue != null) {
+ _logger.LogWarning("Locating filters by the internal propterty name is deprecated. You should use the public attribute name instead.");
+ return publicValue;
+ }
+
+ return null;
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/JsonApiDotNetCore/Services/QueryParser.cs b/src/JsonApiDotNetCore/Services/QueryParser.cs
index 297eb246a0..2a97723347 100644
--- a/src/JsonApiDotNetCore/Services/QueryParser.cs
+++ b/src/JsonApiDotNetCore/Services/QueryParser.cs
@@ -3,69 +3,68 @@
using System.Linq;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Controllers;
-using JsonApiDotNetCore.Extensions;
using JsonApiDotNetCore.Internal;
using JsonApiDotNetCore.Internal.Query;
using JsonApiDotNetCore.Models;
using Microsoft.AspNetCore.Http;
-namespace JsonApiDotNetCore.Services
-{
- public interface IQueryParser
- {
+namespace JsonApiDotNetCore.Services {
+ public interface IQueryParser {
QuerySet Parse(IQueryCollection query);
}
- public class QueryParser : IQueryParser
- {
+ public class QueryParser : IQueryParser {
private readonly IControllerContext _controllerContext;
private readonly JsonApiOptions _options;
+ private const string FILTER = "filter";
+ private const string SORT = "sort";
+ private const string INCLUDE = "include";
+ private const string PAGE = "page";
+ private const string FIELDS = "fields";
+ private const char OPEN_BRACKET = '[';
+ private const char CLOSE_BRACKET = ']';
+ private const char COMMA = ',';
+ private const char COLON = ':';
+ private const string COLON_STR = ":";
+
public QueryParser(
IControllerContext controllerContext,
- JsonApiOptions options)
- {
+ JsonApiOptions options) {
_controllerContext = controllerContext;
_options = options;
}
- public virtual QuerySet Parse(IQueryCollection query)
- {
+ public virtual QuerySet Parse(IQueryCollection query) {
var querySet = new QuerySet();
- var disabledQueries = _controllerContext.GetControllerAttribute()?.QueryParams ?? QueryParams.None;
+ var disabledQueries = _controllerContext.GetControllerAttribute() ? .QueryParams ?? QueryParams.None;
- foreach (var pair in query)
- {
- if (pair.Key.StartsWith("filter"))
- {
+ foreach (var pair in query) {
+ if (pair.Key.StartsWith(FILTER)) {
if (disabledQueries.HasFlag(QueryParams.Filter) == false)
querySet.Filters.AddRange(ParseFilterQuery(pair.Key, pair.Value));
continue;
}
- if (pair.Key.StartsWith("sort"))
- {
+ if (pair.Key.StartsWith(SORT)) {
if (disabledQueries.HasFlag(QueryParams.Sort) == false)
querySet.SortParameters = ParseSortParameters(pair.Value);
continue;
}
- if (pair.Key.StartsWith("include"))
- {
+ if (pair.Key.StartsWith(INCLUDE)) {
if (disabledQueries.HasFlag(QueryParams.Include) == false)
querySet.IncludedRelationships = ParseIncludedRelationships(pair.Value);
continue;
}
- if (pair.Key.StartsWith("page"))
- {
+ if (pair.Key.StartsWith(PAGE)) {
if (disabledQueries.HasFlag(QueryParams.Page) == false)
querySet.PageQuery = ParsePageQuery(querySet.PageQuery, pair.Key, pair.Value);
continue;
}
- if (pair.Key.StartsWith("fields"))
- {
+ if (pair.Key.StartsWith(FIELDS)) {
if (disabledQueries.HasFlag(QueryParams.Fields) == false)
querySet.Fields = ParseFieldsQuery(pair.Key, pair.Value);
continue;
@@ -78,17 +77,15 @@ public virtual QuerySet Parse(IQueryCollection query)
return querySet;
}
- protected virtual List ParseFilterQuery(string key, string value)
- {
+ protected virtual List ParseFilterQuery(string key, string value) {
// expected input = filter[id]=1
// expected input = filter[id]=eq:1
var queries = new List();
- var propertyName = key.Split('[', ']')[1].ToProperCase();
+ var propertyName = key.Split(OPEN_BRACKET, CLOSE_BRACKET) [1];
- var values = value.Split(',');
- foreach (var val in values)
- {
+ var values = value.Split(COMMA);
+ foreach (var val in values) {
(var operation, var filterValue) = ParseFilterOperation(val);
queries.Add(new FilterQuery(propertyName, filterValue, operation));
}
@@ -96,12 +93,11 @@ protected virtual List ParseFilterQuery(string key, string value)
return queries;
}
- protected virtual (string operation, string value) ParseFilterOperation(string value)
- {
+ protected virtual(string operation, string value) ParseFilterOperation(string value) {
if (value.Length < 3)
return (string.Empty, value);
- var operation = value.Split(':');
+ var operation = value.Split(COLON);
if (operation.Length == 1)
return (string.Empty, value);
@@ -111,7 +107,7 @@ protected virtual (string operation, string value) ParseFilterOperation(string v
return (string.Empty, value);
var prefix = operation[0];
- value = string.Join(":", operation.Skip(1));
+ value = string.Join(COLON_STR, operation.Skip(1));
return (prefix, value);
}
@@ -122,12 +118,20 @@ protected virtual PageQuery ParsePageQuery(PageQuery pageQuery, string key, stri
// page[number]=1
pageQuery = pageQuery ?? new PageQuery();
- var propertyName = key.Split('[', ']')[1];
+ var propertyName = key.Split(OPEN_BRACKET, CLOSE_BRACKET)[1];
+
+ const string SIZE = "size";
+ const string NUMBER = "number";
+
+ if (propertyName == SIZE)
+ pageQuery.PageSize = int.TryParse(value, out var pageSize) ?
+ pageSize :
+ throw new JsonApiException(400, $"Invalid page size '{value}'");
- if (propertyName == "size")
- pageQuery.PageSize = Convert.ToInt32(value);
- else if (propertyName == "number")
- pageQuery.PageOffset = Convert.ToInt32(value);
+ else if (propertyName == NUMBER)
+ pageQuery.PageOffset = int.TryParse(value, out var pageOffset) ?
+ pageOffset :
+ throw new JsonApiException(400, $"Invalid page size '{value}'");
return pageQuery;
}
@@ -137,50 +141,62 @@ protected virtual PageQuery ParsePageQuery(PageQuery pageQuery, string key, stri
protected virtual List ParseSortParameters(string value)
{
var sortParameters = new List();
- value.Split(',').ToList().ForEach(p =>
+
+ const char DESCENDING_SORT_OPERATOR = '-';
+ var sortSegments = value.Split(COMMA);
+
+ foreach (var sortSegment in sortSegments)
{
+
+ var propertyName = sortSegment;
var direction = SortDirection.Ascending;
- if (p[0] == '-')
+
+ if (sortSegment[0] == DESCENDING_SORT_OPERATOR)
{
direction = SortDirection.Descending;
- p = p.Substring(1);
+ propertyName = propertyName.Substring(1);
}
- var attribute = GetAttribute(p.ToProperCase());
+ var attribute = GetAttribute(propertyName);
sortParameters.Add(new SortQuery(direction, attribute));
- });
+ };
return sortParameters;
}
- protected virtual List ParseIncludedRelationships(string value)
- {
- if (value.Contains("."))
+ protected virtual List ParseIncludedRelationships(string value) {
+ const string NESTED_DELIMITER = ".";
+ if (value.Contains(NESTED_DELIMITER))
throw new JsonApiException(400, "Deeply nested relationships are not supported");
return value
- .Split(',')
+ .Split(COMMA)
.ToList();
}
protected virtual List ParseFieldsQuery(string key, string value)
{
// expected: fields[TYPE]=prop1,prop2
- var typeName = key.Split('[', ']')[1];
+ var typeName = key.Split(OPEN_BRACKET, CLOSE_BRACKET)[1];
- var includedFields = new List { "Id" };
+ const string ID = "Id";
+ var includedFields = new List { ID };
- if (typeName != _controllerContext.RequestEntity.EntityName)
+ // this will not support nested inclusions, it requires that the typeName is the current request type
+ if (string.Equals(typeName, _controllerContext.RequestEntity.EntityName, StringComparison.OrdinalIgnoreCase) == false)
return includedFields;
- var fields = value.Split(',');
+ var fields = value.Split(COMMA);
foreach (var field in fields)
{
- var internalAttrName = _controllerContext.RequestEntity
+ var attr = _controllerContext.RequestEntity
.Attributes
- .SingleOrDefault(attr => attr.PublicAttributeName == field)
- .InternalAttributeName;
+ .SingleOrDefault(a => string.Equals(a.PublicAttributeName, field, StringComparison.OrdinalIgnoreCase));
+
+ if (attr == null) throw new JsonApiException(400, $"'{_controllerContext.RequestEntity.EntityName}' does not contain '{field}'.");
+
+ var internalAttrName = attr.InternalAttributeName;
includedFields.Add(internalAttrName);
}
@@ -188,11 +204,20 @@ protected virtual List ParseFieldsQuery(string key, string value)
}
protected virtual AttrAttribute GetAttribute(string propertyName)
- => _controllerContext
- .RequestEntity
- .Attributes
- .FirstOrDefault(attr =>
- string.Equals(attr.InternalAttributeName, propertyName, StringComparison.OrdinalIgnoreCase)
- );
+ {
+ try
+ {
+ return _controllerContext
+ .RequestEntity
+ .Attributes
+ .Single(attr =>
+ string.Equals(attr.PublicAttributeName, propertyName, StringComparison.OrdinalIgnoreCase)
+ );
+ }
+ catch (InvalidOperationException e)
+ {
+ throw new JsonApiException(400, $"Attribute '{propertyName}' does not exist on resource '{_controllerContext.RequestEntity.EntityName}'", e);
+ }
+ }
}
-}
\ No newline at end of file
+}
diff --git a/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj b/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj
index b22953dc23..1b40e2dd73 100755
--- a/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj
+++ b/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj
@@ -1,5 +1,5 @@
-
+
$(NetCoreAppVersion)
true
diff --git a/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj b/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj
index 8daa7dc9af..5553a7c1eb 100644
--- a/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj
+++ b/test/NoEntityFrameworkTests/NoEntityFrameworkTests.csproj
@@ -1,5 +1,5 @@
-
+
$(NetCoreAppVersion)
true
diff --git a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs
index eeefa4d857..5096cbac31 100644
--- a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs
+++ b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs
@@ -11,13 +11,10 @@
using Newtonsoft.Json.Serialization;
using Xunit;
-namespace UnitTests.Serialization
-{
- public class JsonApiDeSerializerTests
- {
+namespace UnitTests.Serialization {
+ public class JsonApiDeSerializerTests {
[Fact]
- public void Can_Deserialize_Complex_Types()
- {
+ public void Can_Deserialize_Complex_Types() {
// arrange
var contextGraphBuilder = new ContextGraphBuilder();
contextGraphBuilder.AddResource("test-resource");
@@ -36,17 +33,16 @@ public void Can_Deserialize_Complex_Types()
var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object);
- var content = new Document
- {
- Data = new DocumentData
+ var content = new Document {
+ Data = new DocumentData {
+ Type = "test-resource",
+ Id = "1",
+ Attributes = new Dictionary {
{
- Type = "test-resource",
- Id = "1",
- Attributes = new Dictionary {
- {
- "complex-member", new { compoundName = "testName" }
- }
- }
+ "complex-member",
+ new { compoundName = "testName" }
+ }
+ }
}
};
@@ -59,8 +55,7 @@ public void Can_Deserialize_Complex_Types()
}
[Fact]
- public void Can_Deserialize_Complex_List_Types()
- {
+ public void Can_Deserialize_Complex_List_Types() {
// arrange
var contextGraphBuilder = new ContextGraphBuilder();
contextGraphBuilder.AddResource("test-resource");
@@ -78,19 +73,18 @@ public void Can_Deserialize_Complex_List_Types()
var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object);
- var content = new Document
- {
- Data = new DocumentData
+ var content = new Document {
+ Data = new DocumentData {
+ Type = "test-resource",
+ Id = "1",
+ Attributes = new Dictionary {
{
- Type = "test-resource",
- Id = "1",
- Attributes = new Dictionary {
- {
- "complex-members", new [] {
- new { compoundName = "testName" }
- }
- }
- }
+ "complex-members",
+ new [] {
+ new { compoundName = "testName" }
+ }
+ }
+ }
}
};
@@ -104,8 +98,7 @@ public void Can_Deserialize_Complex_List_Types()
}
[Fact]
- public void Can_Deserialize_Complex_Types_With_Dasherized_Attrs()
- {
+ public void Can_Deserialize_Complex_Types_With_Dasherized_Attrs() {
// arrange
var contextGraphBuilder = new ContextGraphBuilder();
contextGraphBuilder.AddResource("test-resource");
@@ -124,17 +117,16 @@ public void Can_Deserialize_Complex_Types_With_Dasherized_Attrs()
var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object);
- var content = new Document
- {
- Data = new DocumentData
+ var content = new Document {
+ Data = new DocumentData {
+ Type = "test-resource",
+ Id = "1",
+ Attributes = new Dictionary {
{
- Type = "test-resource",
- Id = "1",
- Attributes = new Dictionary {
- {
- "complex-member", new Dictionary { { "compound-name", "testName" } }
- }
- }
+ "complex-member",
+ new Dictionary { { "compound-name", "testName" } }
+ }
+ }
}
};
@@ -147,8 +139,7 @@ public void Can_Deserialize_Complex_Types_With_Dasherized_Attrs()
}
[Fact]
- public void Immutable_Attrs_Are_Not_Included_In_AttributesToUpdate()
- {
+ public void Immutable_Attrs_Are_Not_Included_In_AttributesToUpdate() {
// arrange
var contextGraphBuilder = new ContextGraphBuilder();
contextGraphBuilder.AddResource("test-resource");
@@ -169,18 +160,18 @@ public void Immutable_Attrs_Are_Not_Included_In_AttributesToUpdate()
var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object);
- var content = new Document
- {
- Data = new DocumentData
+ var content = new Document {
+ Data = new DocumentData {
+ Type = "test-resource",
+ Id = "1",
+ Attributes = new Dictionary {
{
- Type = "test-resource",
- Id = "1",
- Attributes = new Dictionary {
- { "complex-member", new Dictionary {
- { "compound-name", "testName" } }
- },
- { "immutable", "value"}
- }
+ "complex-member",
+ new Dictionary { { "compound-name", "testName" }
+ }
+ },
+ { "immutable", "value" }
+ }
}
};
@@ -198,8 +189,7 @@ public void Immutable_Attrs_Are_Not_Included_In_AttributesToUpdate()
}
[Fact]
- public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship()
- {
+ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship() {
// arrange
var contextGraphBuilder = new ContextGraphBuilder();
contextGraphBuilder.AddResource("independents");
@@ -219,15 +209,12 @@ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship()
var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object);
var property = Guid.NewGuid().ToString();
- var content = new Document
- {
- Data = new DocumentData
- {
- Type = "independents",
- Id = "1",
- Attributes = new Dictionary {
- { "property", property }
- }
+ var content = new Document {
+ Data = new DocumentData {
+ Type = "independents",
+ Id = "1",
+ Attributes = new Dictionary { { "property", property }
+ }
}
};
@@ -242,8 +229,7 @@ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship()
}
[Fact]
- public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_Relationship_Body()
- {
+ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_Relationship_Body() {
// arrange
var contextGraphBuilder = new ContextGraphBuilder();
contextGraphBuilder.AddResource("independents");
@@ -263,19 +249,15 @@ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_Rel
var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object);
var property = Guid.NewGuid().ToString();
- var content = new Document
- {
- Data = new DocumentData
- {
- Type = "independents",
- Id = "1",
- Attributes = new Dictionary {
- { "property", property }
- },
- // a common case for this is deserialization in unit tests
- Relationships = new Dictionary {
- { "dependent", new RelationshipData { } }
- }
+ var content = new Document {
+ Data = new DocumentData {
+ Type = "independents",
+ Id = "1",
+ Attributes = new Dictionary { { "property", property }
+ },
+ // a common case for this is deserialization in unit tests
+ Relationships = new Dictionary { { "dependent", new RelationshipData { } }
+ }
}
};
@@ -289,34 +271,29 @@ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_Rel
Assert.Equal(property, result.Property);
}
- private class TestResource : Identifiable
- {
+ private class TestResource : Identifiable {
[Attr("complex-member")]
public ComplexType ComplexMember { get; set; }
- [Attr("immutable", isImmutable: true)]
+ [Attr("immutable", isImmutable : true)]
public string Immutable { get; set; }
}
- private class TestResourceWithList : Identifiable
- {
+ private class TestResourceWithList : Identifiable {
[Attr("complex-members")]
public List ComplexMembers { get; set; }
}
- private class ComplexType
- {
+ private class ComplexType {
public string CompoundName { get; set; }
}
- private class Independent : Identifiable
- {
+ private class Independent : Identifiable {
[Attr("property")] public string Property { get; set; }
[HasOne("dependent")] public Dependent Dependent { get; set; }
}
- private class Dependent : Identifiable
- {
+ private class Dependent : Identifiable {
[HasOne("independent")] public Independent Independent { get; set; }
public int IndependentId { get; set; }
}
diff --git a/test/UnitTests/Services/QueryParser_Tests.cs b/test/UnitTests/Services/QueryParser_Tests.cs
index a64c5b3692..64c9830f2b 100644
--- a/test/UnitTests/Services/QueryParser_Tests.cs
+++ b/test/UnitTests/Services/QueryParser_Tests.cs
@@ -2,6 +2,8 @@
using System.Linq;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.Internal;
+using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
@@ -43,7 +45,7 @@ public void Can_Build_Filters()
var querySet = queryParser.Parse(_queryCollectionMock.Object);
// assert
- Assert.Equal("value", querySet.Filters.Single(f => f.Key == "Key").Value);
+ Assert.Equal("value", querySet.Filters.Single(f => f.Attribute == "key").Value);
}
[Fact]
@@ -69,8 +71,8 @@ public void Filters_Properly_Parses_DateTime_With_Operation()
var querySet = queryParser.Parse(_queryCollectionMock.Object);
// assert
- Assert.Equal(dt, querySet.Filters.Single(f => f.Key == "Key").Value);
- Assert.Equal("le", querySet.Filters.Single(f => f.Key == "Key").Operation);
+ Assert.Equal(dt, querySet.Filters.Single(f => f.Attribute == "key").Value);
+ Assert.Equal("le", querySet.Filters.Single(f => f.Attribute == "key").Operation);
}
[Fact]
@@ -96,8 +98,8 @@ public void Filters_Properly_Parses_DateTime_Without_Operation()
var querySet = queryParser.Parse(_queryCollectionMock.Object);
// assert
- Assert.Equal(dt, querySet.Filters.Single(f => f.Key == "Key").Value);
- Assert.Equal(string.Empty, querySet.Filters.Single(f => f.Key == "Key").Operation);
+ Assert.Equal(dt, querySet.Filters.Single(f => f.Attribute == "key").Value);
+ Assert.Equal(string.Empty, querySet.Filters.Single(f => f.Attribute == "key").Operation);
}
[Fact]
@@ -224,5 +226,133 @@ public void Can_Disable_Fields()
// assert
Assert.Empty(querySet.Fields);
}
+
+ [Fact]
+ public void Can_Parse_Fields_Query()
+ {
+ // arrange
+ const string type = "articles";
+ const string attrName = "some-field";
+ const string internalAttrName = "SomeField";
+
+ var query = new Dictionary { { $"fields[{type}]", new StringValues(attrName) } };
+
+ _queryCollectionMock
+ .Setup(m => m.GetEnumerator())
+ .Returns(query.GetEnumerator());
+
+ _controllerContextMock
+ .Setup(m => m.RequestEntity)
+ .Returns(new ContextEntity
+ {
+ EntityName = type,
+ Attributes = new List
+ {
+ new AttrAttribute(attrName)
+ {
+ InternalAttributeName = internalAttrName
+ }
+ }
+ });
+
+ var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions());
+
+ // act
+ var querySet = queryParser.Parse(_queryCollectionMock.Object);
+
+ // assert
+ Assert.NotEmpty(querySet.Fields);
+ Assert.Equal(2, querySet.Fields.Count);
+ Assert.Equal("Id", querySet.Fields[0]);
+ Assert.Equal(internalAttrName, querySet.Fields[1]);
+ }
+
+ [Fact]
+ public void Throws_JsonApiException_If_Field_DoesNotExist()
+ {
+ // arrange
+ const string type = "articles";
+ const string attrName = "dne";
+
+ var query = new Dictionary { { $"fields[{type}]", new StringValues(attrName) } };
+
+ _queryCollectionMock
+ .Setup(m => m.GetEnumerator())
+ .Returns(query.GetEnumerator());
+
+ _controllerContextMock
+ .Setup(m => m.RequestEntity)
+ .Returns(new ContextEntity
+ {
+ EntityName = type,
+ Attributes = new List()
+ });
+
+ var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions());
+
+ // act , assert
+ var ex = Assert.Throws(() => queryParser.Parse(_queryCollectionMock.Object));
+ Assert.Equal(400, ex.GetStatusCode());
+ }
+
+ [Theory]
+ [InlineData("1", 1, false)]
+ [InlineData("abcde", 0, true)]
+ [InlineData("", 0, true)]
+ public void Can_Parse_Page_Size_Query(string value, int expectedValue, bool shouldThrow)
+ {
+ // arrange
+ var query = new Dictionary
+ { { "page[size]", new StringValues(value) }
+ };
+
+ _queryCollectionMock
+ .Setup(m => m.GetEnumerator())
+ .Returns(query.GetEnumerator());
+
+ var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions());
+
+ // act
+ if (shouldThrow)
+ {
+ var ex = Assert.Throws(() => queryParser.Parse(_queryCollectionMock.Object));
+ Assert.Equal(400, ex.GetStatusCode());
+ }
+ else
+ {
+ var querySet = queryParser.Parse(_queryCollectionMock.Object);
+ Assert.Equal(expectedValue, querySet.PageQuery.PageSize);
+ }
+ }
+
+ [Theory]
+ [InlineData("1", 1, false)]
+ [InlineData("abcde", 0, true)]
+ [InlineData("", 0, true)]
+ public void Can_Parse_Page_Number_Query(string value, int expectedValue, bool shouldThrow)
+ {
+ // arrange
+ var query = new Dictionary
+ { { "page[number]", new StringValues(value) }
+ };
+
+ _queryCollectionMock
+ .Setup(m => m.GetEnumerator())
+ .Returns(query.GetEnumerator());
+
+ var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions());
+
+ // act
+ if (shouldThrow)
+ {
+ var ex = Assert.Throws(() => queryParser.Parse(_queryCollectionMock.Object));
+ Assert.Equal(400, ex.GetStatusCode());
+ }
+ else
+ {
+ var querySet = queryParser.Parse(_queryCollectionMock.Object);
+ Assert.Equal(expectedValue, querySet.PageQuery.PageOffset);
+ }
+ }
}
-}
+}
\ No newline at end of file
diff --git a/test/UnitTests/UnitTests.csproj b/test/UnitTests/UnitTests.csproj
index dcd659d2df..4848455eac 100644
--- a/test/UnitTests/UnitTests.csproj
+++ b/test/UnitTests/UnitTests.csproj
@@ -1,4 +1,5 @@
+
$(NetCoreAppVersion)
false
@@ -13,4 +14,4 @@
-
\ No newline at end of file
+