diff --git a/Directory.Build.props b/Directory.Build.props index 6eb8cf3e1f..9aaa62aba4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,6 @@ netcoreapp3.1 - netstandard2.1 3.1.* 3.1.* 3.1.* @@ -10,6 +9,7 @@ $(NoWarn);1591 true + true diff --git a/README.md b/README.md index 79233e1bda..bc3dd8e80c 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,10 @@ public class Article : Identifiable public class ArticlesController : JsonApiController
{ public ArticlesController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, IResourceService
resourceService, ILoggerFactory loggerFactory) - : base(jsonApiOptions, resourceService, loggerFactory) + : base(options, resourceService, loggerFactory) { } } ``` diff --git a/benchmarks/BenchmarkResource.cs b/benchmarks/BenchmarkResource.cs index d7dc1bcce6..0353078601 100644 --- a/benchmarks/BenchmarkResource.cs +++ b/benchmarks/BenchmarkResource.cs @@ -1,11 +1,11 @@ -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace Benchmarks { public sealed class BenchmarkResource : Identifiable { - [Attr(BenchmarkResourcePublicNames.NameAttr)] + [Attr(PublicName = BenchmarkResourcePublicNames.NameAttr)] public string Name { get; set; } [HasOne] diff --git a/benchmarks/DependencyFactory.cs b/benchmarks/DependencyFactory.cs index 0492a9a185..87680c7ba1 100644 --- a/benchmarks/DependencyFactory.cs +++ b/benchmarks/DependencyFactory.cs @@ -1,9 +1,6 @@ using System; -using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services.Contract; +using JsonApiDotNetCore.Resources; using Microsoft.Extensions.Logging.Abstractions; using Moq; @@ -14,7 +11,7 @@ internal static class DependencyFactory public static IResourceGraph CreateResourceGraph(IJsonApiOptions options) { IResourceGraphBuilder builder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance); - builder.AddResource(BenchmarkResourcePublicNames.Type); + builder.Add(BenchmarkResourcePublicNames.Type); return builder.Build(); } diff --git a/benchmarks/LinkBuilder/LinkBuilderGetNamespaceFromPathBenchmarks.cs b/benchmarks/LinkBuilder/LinkBuilderGetNamespaceFromPathBenchmarks.cs index 47751425d9..60b5f31ce1 100644 --- a/benchmarks/LinkBuilder/LinkBuilderGetNamespaceFromPathBenchmarks.cs +++ b/benchmarks/LinkBuilder/LinkBuilderGetNamespaceFromPathBenchmarks.cs @@ -1,6 +1,6 @@ -using BenchmarkDotNet.Attributes; using System; using System.Text; +using BenchmarkDotNet.Attributes; namespace Benchmarks.LinkBuilder { diff --git a/benchmarks/Query/QueryParserBenchmarks.cs b/benchmarks/Query/QueryParserBenchmarks.cs index 64f144b9e5..b5be39eb03 100644 --- a/benchmarks/Query/QueryParserBenchmarks.cs +++ b/benchmarks/Query/QueryParserBenchmarks.cs @@ -3,11 +3,10 @@ using System.ComponentModel.Design; using BenchmarkDotNet.Attributes; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Internal.QueryStrings; +using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.QueryStrings; -using JsonApiDotNetCore.RequestServices; +using JsonApiDotNetCore.QueryStrings.Internal; +using JsonApiDotNetCore.Resources; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging.Abstractions; @@ -30,20 +29,19 @@ public QueryParserBenchmarks() IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options); - var currentRequest = new CurrentRequest + var request = new JsonApiRequest { PrimaryResource = resourceGraph.GetResourceContext(typeof(BenchmarkResource)) }; - _queryStringReaderForSort = CreateQueryParameterDiscoveryForSort(resourceGraph, currentRequest, options, _queryStringAccessor); - _queryStringReaderForAll = CreateQueryParameterDiscoveryForAll(resourceGraph, currentRequest, options, _queryStringAccessor); + _queryStringReaderForSort = CreateQueryParameterDiscoveryForSort(resourceGraph, request, options, _queryStringAccessor); + _queryStringReaderForAll = CreateQueryParameterDiscoveryForAll(resourceGraph, request, options, _queryStringAccessor); } private static QueryStringReader CreateQueryParameterDiscoveryForSort(IResourceGraph resourceGraph, - CurrentRequest currentRequest, - IJsonApiOptions options, FakeRequestQueryStringAccessor queryStringAccessor) + JsonApiRequest request, IJsonApiOptions options, FakeRequestQueryStringAccessor queryStringAccessor) { - var sortReader = new SortQueryStringParameterReader(currentRequest, resourceGraph); + var sortReader = new SortQueryStringParameterReader(request, resourceGraph); var readers = new List { @@ -54,14 +52,14 @@ private static QueryStringReader CreateQueryParameterDiscoveryForSort(IResourceG } private static QueryStringReader CreateQueryParameterDiscoveryForAll(IResourceGraph resourceGraph, - CurrentRequest currentRequest, IJsonApiOptions options, FakeRequestQueryStringAccessor queryStringAccessor) + JsonApiRequest request, IJsonApiOptions options, FakeRequestQueryStringAccessor queryStringAccessor) { var resourceFactory = new ResourceFactory(new ServiceContainer()); - var filterReader = new FilterQueryStringParameterReader(currentRequest, resourceGraph, resourceFactory, options); - var sortReader = new SortQueryStringParameterReader(currentRequest, resourceGraph); - var sparseFieldSetReader = new SparseFieldSetQueryStringParameterReader(currentRequest, resourceGraph); - var paginationReader = new PaginationQueryStringParameterReader(currentRequest, resourceGraph, options); + var filterReader = new FilterQueryStringParameterReader(request, resourceGraph, resourceFactory, options); + var sortReader = new SortQueryStringParameterReader(request, resourceGraph); + var sparseFieldSetReader = new SparseFieldSetQueryStringParameterReader(request, resourceGraph); + var paginationReader = new PaginationQueryStringParameterReader(request, resourceGraph, options); var defaultsReader = new DefaultsQueryStringParameterReader(options); var nullsReader = new NullsQueryStringParameterReader(options); diff --git a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs index 5908110d56..6a373eb73c 100644 --- a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs +++ b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs @@ -3,11 +3,9 @@ using System.ComponentModel.Design; using BenchmarkDotNet.Attributes; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Serialization.Server; +using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; diff --git a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs index c334e59235..7f8b961548 100644 --- a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs +++ b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs @@ -1,13 +1,11 @@ using System; using BenchmarkDotNet.Attributes; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Internal.QueryStrings; +using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.RequestServices; +using JsonApiDotNetCore.QueryStrings.Internal; using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Serialization.Server; -using JsonApiDotNetCore.Serialization.Server.Builders; +using JsonApiDotNetCore.Serialization.Building; using Moq; namespace Benchmarks.Serialization @@ -41,11 +39,11 @@ public JsonApiSerializerBenchmarks() private static FieldsToSerialize CreateFieldsToSerialize(IResourceGraph resourceGraph) { - var currentRequest = new CurrentRequest(); + var request = new JsonApiRequest(); var constraintProviders = new IQueryConstraintProvider[] { - new SparseFieldSetQueryStringParameterReader(currentRequest, resourceGraph) + new SparseFieldSetQueryStringParameterReader(request, resourceGraph) }; var resourceDefinitionProvider = DependencyFactory.CreateResourceDefinitionProvider(resourceGraph); diff --git a/docs/api/index.md b/docs/api/index.md index 51c288d491..6d92763517 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -5,5 +5,5 @@ This section documents the package API and is generated from the XML source comm ## Common APIs - [`JsonApiOptions`](JsonApiDotNetCore.Configuration.JsonApiOptions.yml) -- [`IResourceGraph`](JsonApiDotNetCore.Internal.Contracts.IResourceGraph.yml) -- [`ResourceDefinition`](JsonApiDotNetCore.Models.ResourceDefinition-1.yml) +- [`IResourceGraph`](JsonApiDotNetCore.Configuration.IResourceGraph.yml) +- [`ResourceDefinition`](JsonApiDotNetCore.Resources.ResourceDefinition-1.yml) diff --git a/docs/docfx.json b/docs/docfx.json index 68351f5471..45e6c353dd 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -10,7 +10,7 @@ "dest": "api", "disableGitFeatures": false, "properties": { - "targetFramework": "netstandard2.0" + "targetFramework": "netcoreapp3.1" } } ], diff --git a/docs/getting-started/step-by-step.md b/docs/getting-started/step-by-step.md index ac0b03fc74..2520f4f0dc 100644 --- a/docs/getting-started/step-by-step.md +++ b/docs/getting-started/step-by-step.md @@ -68,10 +68,10 @@ where `T` is the model that inherits from `Identifiable` public class PeopleController : JsonApiController { public PeopleController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } ``` diff --git a/docs/usage/extensibility/controllers.md b/docs/usage/extensibility/controllers.md index b6e4e8d686..1040d33f1b 100644 --- a/docs/usage/extensibility/controllers.md +++ b/docs/usage/extensibility/controllers.md @@ -6,10 +6,10 @@ You need to create controllers that inherit from `JsonApiController` public class ArticlesController : JsonApiController
{ public ArticlesController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService
resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } ``` @@ -23,11 +23,11 @@ public class ArticlesController : JsonApiController //---------------------------------------------------------- ^^^^ { public ArticlesController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) //----------------------- ^^^^ - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } ``` @@ -44,10 +44,10 @@ This approach is ok, but introduces some boilerplate that can easily be avoided. public class ArticlesController : BaseJsonApiController
{ public ArticlesController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService
resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } [HttpGet] @@ -81,10 +81,10 @@ An attempt to use one blacklisted methods will result in a HTTP 405 Method Not A public class ArticlesController : BaseJsonApiController
{ public ArticlesController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService
resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } ``` @@ -101,10 +101,10 @@ For more information about resource injection, see the next section titled Resou public class ReportsController : BaseJsonApiController { public ReportsController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } [HttpGet] diff --git a/docs/usage/extensibility/services.md b/docs/usage/extensibility/services.md index 91c41881e0..84d7ac3897 100644 --- a/docs/usage/extensibility/services.md +++ b/docs/usage/extensibility/services.md @@ -148,11 +148,11 @@ Then in the controller, you should inherit from the base controller and pass the public class ArticlesController : BaseJsonApiController
{ public ArticlesController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, ICreateService create, IDeleteService delete) - : base(jsonApiOptions, loggerFactory, create: create, delete: delete) + : base(options, loggerFactory, create: create, delete: delete) { } [HttpPost] diff --git a/docs/usage/routing.md b/docs/usage/routing.md index f6ddc72e37..b4261d2516 100644 --- a/docs/usage/routing.md +++ b/docs/usage/routing.md @@ -30,10 +30,10 @@ You can disable the default casing convention and specify your own template by u public class CamelCasedModelsController : JsonApiController { public CamelCasedModelsController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } ``` diff --git a/src/Examples/GettingStarted/Controllers/ArticlesController.cs b/src/Examples/GettingStarted/Controllers/ArticlesController.cs index fc79e4bed8..ad73a9e9bd 100644 --- a/src/Examples/GettingStarted/Controllers/ArticlesController.cs +++ b/src/Examples/GettingStarted/Controllers/ArticlesController.cs @@ -9,10 +9,10 @@ namespace GettingStarted.Controllers public sealed class ArticlesController : JsonApiController
{ public ArticlesController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService
resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } } diff --git a/src/Examples/GettingStarted/Controllers/PeopleController.cs b/src/Examples/GettingStarted/Controllers/PeopleController.cs index 05baa40b68..0337a159d3 100644 --- a/src/Examples/GettingStarted/Controllers/PeopleController.cs +++ b/src/Examples/GettingStarted/Controllers/PeopleController.cs @@ -9,10 +9,10 @@ namespace GettingStarted.Controllers public sealed class PeopleController : JsonApiController { public PeopleController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } } diff --git a/src/Examples/GettingStarted/Models/Article.cs b/src/Examples/GettingStarted/Models/Article.cs index 3c0226ec2c..eb7d0d8093 100644 --- a/src/Examples/GettingStarted/Models/Article.cs +++ b/src/Examples/GettingStarted/Models/Article.cs @@ -1,5 +1,5 @@ -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace GettingStarted.Models { diff --git a/src/Examples/GettingStarted/Models/Person.cs b/src/Examples/GettingStarted/Models/Person.cs index 2bb49bfb2b..161ed14e58 100644 --- a/src/Examples/GettingStarted/Models/Person.cs +++ b/src/Examples/GettingStarted/Models/Person.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace GettingStarted.Models { diff --git a/src/Examples/GettingStarted/Startup.cs b/src/Examples/GettingStarted/Startup.cs index dd499af5ba..5d9c096457 100644 --- a/src/Examples/GettingStarted/Startup.cs +++ b/src/Examples/GettingStarted/Startup.cs @@ -1,6 +1,6 @@ using GettingStarted.Data; using GettingStarted.Models; -using JsonApiDotNetCore; +using JsonApiDotNetCore.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs index 4ae287c670..a553851ccd 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs @@ -9,10 +9,10 @@ namespace JsonApiDotNetCoreExample.Controllers public sealed class ArticlesController : JsonApiController
{ public ArticlesController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService
resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/AuthorsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/AuthorsController.cs index 273a62f2b7..789c31cb95 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/AuthorsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/AuthorsController.cs @@ -9,10 +9,10 @@ namespace JsonApiDotNetCoreExample.Controllers public sealed class AuthorsController : JsonApiController { public AuthorsController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/BlogsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/BlogsController.cs index e98bdad0f9..824b7a30a6 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/BlogsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/BlogsController.cs @@ -9,10 +9,10 @@ namespace JsonApiDotNetCoreExample.Controllers public sealed class BlogsController : JsonApiController { public BlogsController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/CountriesController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/CountriesController.cs index 5da41f2726..2f37dcb387 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/CountriesController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/CountriesController.cs @@ -1,19 +1,21 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.QueryStrings; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCoreExample.Controllers { - [DisableQuery(StandardQueryStringParameters.Sort | StandardQueryStringParameters.Page)] + [DisableQueryString(StandardQueryStringParameters.Sort | StandardQueryStringParameters.Page)] public sealed class CountriesController : JsonApiController { public CountriesController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/KebabCasedModelsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/KebabCasedModelsController.cs index 3f1ac5e2ed..a473241247 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/KebabCasedModelsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/KebabCasedModelsController.cs @@ -9,10 +9,10 @@ namespace JsonApiDotNetCoreExample.Controllers public sealed class KebabCasedModelsController : JsonApiController { public KebabCasedModelsController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs index 67ec633b6e..152628ad96 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs @@ -11,10 +11,10 @@ namespace JsonApiDotNetCoreExample.Controllers public sealed class PassportsController : BaseJsonApiController { public PassportsController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } [HttpGet] diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs index eae404b7bc..4b0116ece6 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs @@ -9,10 +9,10 @@ namespace JsonApiDotNetCoreExample.Controllers public sealed class PeopleController : JsonApiController { public PeopleController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/PersonRolesController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/PersonRolesController.cs index 4dfb9232b0..75c930126f 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/PersonRolesController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/PersonRolesController.cs @@ -9,10 +9,10 @@ namespace JsonApiDotNetCoreExample.Controllers public sealed class PersonRolesController : JsonApiController { public PersonRolesController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/Restricted/ReadOnlyController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/Restricted/ReadOnlyController.cs index 66e67fc9d6..cbdbdbc2a8 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/Restricted/ReadOnlyController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/Restricted/ReadOnlyController.cs @@ -1,5 +1,6 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Controllers.Annotations; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Mvc; @@ -12,10 +13,10 @@ namespace JsonApiDotNetCoreExample.Controllers.Restricted public class ReadOnlyController : BaseJsonApiController
{ public ReadOnlyController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService
resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } [HttpGet] @@ -36,10 +37,10 @@ public ReadOnlyController( public class NoHttpPostController : BaseJsonApiController
{ public NoHttpPostController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService
resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } [HttpGet] @@ -60,10 +61,10 @@ public NoHttpPostController( public class NoHttpPatchController : BaseJsonApiController
{ public NoHttpPatchController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService
resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } [HttpGet] @@ -84,10 +85,10 @@ public NoHttpPatchController( public class NoHttpDeleteController : BaseJsonApiController
{ public NoHttpDeleteController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService
resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } [HttpGet] diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TagsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TagsController.cs index c134c8422d..bccf2192d6 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TagsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TagsController.cs @@ -1,19 +1,20 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Controllers.Annotations; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCoreExample.Controllers { - [DisableQuery("skipCache")] + [DisableQueryString("skipCache")] public sealed class TagsController : JsonApiController { public TagsController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/ThrowingResourcesController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/ThrowingResourcesController.cs index 3c87a76777..8b662cde09 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/ThrowingResourcesController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/ThrowingResourcesController.cs @@ -9,10 +9,10 @@ namespace JsonApiDotNetCoreExample.Controllers public sealed class ThrowingResourcesController : JsonApiController { public ThrowingResourcesController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs index 0c748d385a..a6c130940b 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Repositories; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Mvc; @@ -17,11 +17,11 @@ public sealed class TodoCollectionsController : JsonApiController resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { _dbResolver = contextResolver; } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs index d21f6c016d..dbd9057a12 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs @@ -9,10 +9,10 @@ namespace JsonApiDotNetCoreExample.Controllers public sealed class TodoItemsController : JsonApiController { public TodoItemsController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs index 14a02fefae..fb75b37226 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs @@ -2,9 +2,10 @@ using System.Net; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Mvc; diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs index 24ed45bc67..2480d40f04 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs @@ -2,8 +2,9 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Mvc; @@ -15,10 +16,10 @@ public abstract class AbstractTodoItemsController : BaseJsonApiController where T : class, IIdentifiable { protected AbstractTodoItemsController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService service) - : base(jsonApiOptions, loggerFactory, service) + : base(options, loggerFactory, service) { } } @@ -27,10 +28,10 @@ protected AbstractTodoItemsController( public class TodoItemsTestController : AbstractTodoItemsController { public TodoItemsTestController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService service) - : base(jsonApiOptions, loggerFactory, service) + : base(options, loggerFactory, service) { } [HttpGet] diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs index e07456cc90..2411879bb7 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs @@ -9,20 +9,20 @@ namespace JsonApiDotNetCoreExample.Controllers public sealed class UsersController : JsonApiController { public UsersController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } public sealed class SuperUsersController : JsonApiController { public SuperUsersController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/VisasController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/VisasController.cs index 2063710141..1cb637160e 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/VisasController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/VisasController.cs @@ -9,10 +9,10 @@ namespace JsonApiDotNetCoreExample.Controllers public sealed class VisasController : JsonApiController { public VisasController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleDefinition.cs index 3eddbc41e6..b28817b6f0 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleDefinition.cs @@ -1,11 +1,11 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Hooks; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample.Models; namespace JsonApiDotNetCoreExample.Definitions diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableDefinition.cs index a4649807dd..02f66eafaf 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableDefinition.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample.Models; namespace JsonApiDotNetCoreExample.Definitions diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportDefinition.cs index dbe26ddcca..84c1e8ec95 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportDefinition.cs @@ -1,11 +1,11 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Hooks; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample.Models; namespace JsonApiDotNetCoreExample.Definitions diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonDefinition.cs index 9364e71505..7041e1fd42 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonDefinition.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Hooks; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Resources; using JsonApiDotNetCoreExample.Models; namespace JsonApiDotNetCoreExample.Definitions @@ -22,7 +22,7 @@ public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary

().ToList().ForEach(kvp => DisallowLocked(kvp.Value)); } - public Dictionary GetMeta() + public IReadOnlyDictionary GetMeta() { return new Dictionary { { "copyright", "Copyright 2015 Example Corp." }, diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/TagDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/TagDefinition.cs index b1cea0ec72..bcc423c1d5 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/TagDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/TagDefinition.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Hooks; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Resources; using JsonApiDotNetCoreExample.Models; namespace JsonApiDotNetCoreExample.Definitions diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoDefinition.cs index 6f27e716a3..bdcb687b86 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoDefinition.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Hooks; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample.Models; namespace JsonApiDotNetCoreExample.Definitions diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Address.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Address.cs index 5deccaf48d..a84436df31 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Address.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Address.cs @@ -1,5 +1,5 @@ -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs index d835965eb1..cdfc216110 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/ArticleTag.cs b/src/Examples/JsonApiDotNetCoreExample/Models/ArticleTag.cs index 39d1dca693..317ecf5e65 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/ArticleTag.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/ArticleTag.cs @@ -1,5 +1,5 @@ -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs index a089730f2b..88793599e9 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs @@ -1,7 +1,7 @@ using System; -using JsonApiDotNetCore.Models; using System.Collections.Generic; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Blog.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Blog.cs index 15a01e6584..0330150ca3 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Blog.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Blog.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Country.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Country.cs index 831b464b44..0a30443aed 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Country.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Country.cs @@ -1,5 +1,5 @@ -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/KebabCasedModel.cs b/src/Examples/JsonApiDotNetCoreExample/Models/KebabCasedModel.cs index 85502e17ff..d9e4c4bbf9 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/KebabCasedModel.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/KebabCasedModel.cs @@ -1,5 +1,5 @@ -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs index 195df3528c..c42367aaff 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCoreExample.Data; using Microsoft.AspNetCore.Authentication; @@ -14,7 +14,7 @@ public class Passport : Identifiable private readonly ISystemClock _systemClock; private int? _socialSecurityNumber; - protected override string GetStringId(object value) + protected override string GetStringId(int value) { return HexadecimalObfuscationCodec.Encode(value); } @@ -62,7 +62,7 @@ public string BirthCountryName [EagerLoad] public Country BirthCountry { get; set; } - [Attr(AttrCapabilities.All & ~AttrCapabilities.AllowChange)] + [Attr(Capabilities = AttrCapabilities.All & ~AttrCapabilities.AllowChange)] [NotMapped] public string GrantedVisaCountries => GrantedVisas == null || !GrantedVisas.Any() ? null diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs index 979c169d29..db610ac0f2 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs @@ -1,8 +1,7 @@ using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { @@ -38,7 +37,7 @@ public string FirstName [Attr] public string LastName { get; set; } - [Attr("the-Age")] + [Attr(PublicName = "the-Age")] public int Age { get; set; } [Attr] @@ -67,7 +66,7 @@ public string FirstName public TodoItem StakeHolderTodoItem { get; set; } public int? StakeHolderTodoItemId { get; set; } - [HasOne(links: Links.All, canInclude: false)] + [HasOne(Links = LinkTypes.All, CanInclude = false)] public TodoItem UnIncludeableItem { get; set; } [HasOne] diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Revision.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Revision.cs index e475632a36..7b9beb3f7c 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Revision.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Revision.cs @@ -1,6 +1,6 @@ using System; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Tag.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Tag.cs index ccf0da0b75..21b74c0fd4 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Tag.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Tag.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/ThrowingResource.cs b/src/Examples/JsonApiDotNetCoreExample/Models/ThrowingResource.cs index 935dcffb29..cf2f963e2a 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/ThrowingResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/ThrowingResource.cs @@ -1,9 +1,9 @@ using System; using System.Diagnostics; using System.Linq; -using JsonApiDotNetCore.Formatters; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization; namespace JsonApiDotNetCoreExample.Models { diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs b/src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs index d19201cc54..c87ea2e5bb 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { @@ -33,13 +33,13 @@ public string AlwaysChangingValue [Attr] public DateTime CreatedDate { get; set; } - [Attr(AttrCapabilities.All & ~(AttrCapabilities.AllowFilter | AttrCapabilities.AllowSort))] + [Attr(Capabilities = AttrCapabilities.All & ~(AttrCapabilities.AllowFilter | AttrCapabilities.AllowSort))] public DateTime? AchievedDate { get; set; } [Attr] public DateTime? UpdatedDate { get; set; } - [Attr(AttrCapabilities.All & ~AttrCapabilities.AllowChange)] + [Attr(Capabilities = AttrCapabilities.All & ~AttrCapabilities.AllowChange)] public string CalculatedValue => "calculated"; [Attr] diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs b/src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs index bd2aed6b10..9b0a515f25 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/User.cs b/src/Examples/JsonApiDotNetCoreExample/Models/User.cs index 79f4c8cc89..2760278844 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/User.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/User.cs @@ -1,6 +1,6 @@ using System; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCoreExample.Data; using Microsoft.AspNetCore.Authentication; @@ -13,7 +13,7 @@ public class User : Identifiable [Attr] public string UserName { get; set; } - [Attr(AttrCapabilities.AllowChange)] + [Attr(Capabilities = AttrCapabilities.AllowChange)] public string Password { get => _password; diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs index f67112db0a..781a391a8e 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs @@ -1,6 +1,6 @@ using System; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExample.Models { diff --git a/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs b/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs index 31dba7ef66..430b65e27e 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs @@ -1,14 +1,13 @@ +using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Hooks; +using JsonApiDotNetCore.Hooks.Internal; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.Extensions.Logging; -using System.Threading.Tasks; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.RequestServices; -using JsonApiDotNetCore.RequestServices.Contracts; namespace JsonApiDotNetCoreExample.Services { @@ -20,11 +19,11 @@ public CustomArticleService( IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, - ICurrentRequest currentRequest, + IJsonApiRequest request, IResourceChangeTracker

resourceChangeTracker, IResourceFactory resourceFactory, IResourceHookExecutor hookExecutor = null) - : base(repository, queryLayerComposer, paginationContext, options, loggerFactory, currentRequest, + : base(repository, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, resourceFactory, hookExecutor) { } diff --git a/src/Examples/JsonApiDotNetCoreExample/Services/SkipCacheQueryStringParameterReader.cs b/src/Examples/JsonApiDotNetCoreExample/Services/SkipCacheQueryStringParameterReader.cs index a4dcaa50c2..95e2676d9b 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Services/SkipCacheQueryStringParameterReader.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Services/SkipCacheQueryStringParameterReader.cs @@ -1,6 +1,6 @@ using System.Linq; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.QueryStrings; using Microsoft.Extensions.Primitives; @@ -12,9 +12,9 @@ public class SkipCacheQueryStringParameterReader : IQueryStringParameterReader public bool SkipCache { get; private set; } - public bool IsEnabled(DisableQueryAttribute disableQueryAttribute) + public bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) { - return !disableQueryAttribute.ParameterNames.Contains(_skipCacheParameterName.ToLowerInvariant()); + return !disableQueryStringAttribute.ParameterNames.Contains(_skipCacheParameterName.ToLowerInvariant()); } public bool CanRead(string parameterName) diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs index 297f1e8bfc..18d40e9ccd 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs @@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreExample { /// /// Empty startup class, required for integration tests. - /// Changes in .NET Core 3 no longer allow Startup class to be defined in test projects. See https://github.com/aspnet/AspNetCore/issues/15373. + /// Changes in ASP.NET Core 3 no longer allow Startup class to be defined in test projects. See https://github.com/aspnet/AspNetCore/issues/15373. /// public abstract class EmptyStartup { diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs index 0b0d0eba07..c6b0cfd87b 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs @@ -1,15 +1,14 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using JsonApiDotNetCoreExample.Data; -using Microsoft.EntityFrameworkCore; using System; -using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.QueryStrings; +using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Services; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Converters; diff --git a/src/Examples/NoEntityFrameworkExample/Controllers/WorkItemsController.cs b/src/Examples/NoEntityFrameworkExample/Controllers/WorkItemsController.cs index 6d1abae1f0..79b994d479 100644 --- a/src/Examples/NoEntityFrameworkExample/Controllers/WorkItemsController.cs +++ b/src/Examples/NoEntityFrameworkExample/Controllers/WorkItemsController.cs @@ -1,18 +1,18 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; -using NoEntityFrameworkExample.Models; using Microsoft.Extensions.Logging; +using NoEntityFrameworkExample.Models; namespace NoEntityFrameworkExample.Controllers { public sealed class WorkItemsController : JsonApiController { public WorkItemsController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } } diff --git a/src/Examples/NoEntityFrameworkExample/Models/WorkItem.cs b/src/Examples/NoEntityFrameworkExample/Models/WorkItem.cs index 2c00c8df6d..a3a929bd5a 100644 --- a/src/Examples/NoEntityFrameworkExample/Models/WorkItem.cs +++ b/src/Examples/NoEntityFrameworkExample/Models/WorkItem.cs @@ -1,6 +1,6 @@ using System; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace NoEntityFrameworkExample.Models { diff --git a/src/Examples/NoEntityFrameworkExample/Startup.cs b/src/Examples/NoEntityFrameworkExample/Startup.cs index ef3bcb5287..4754f6003d 100644 --- a/src/Examples/NoEntityFrameworkExample/Startup.cs +++ b/src/Examples/NoEntityFrameworkExample/Startup.cs @@ -1,5 +1,5 @@ using System; -using JsonApiDotNetCore; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; @@ -26,7 +26,7 @@ public void ConfigureServices(IServiceCollection services) { services.AddJsonApi( options => options.Namespace = "api/v1", - resources: builder => builder.AddResource("workItems") + resources: builder => builder.Add("workItems") ); services.AddScoped, WorkItemService>(); diff --git a/src/Examples/ReportsExample/Controllers/ReportsController.cs b/src/Examples/ReportsExample/Controllers/ReportsController.cs index 511f691f68..c80aba4680 100644 --- a/src/Examples/ReportsExample/Controllers/ReportsController.cs +++ b/src/Examples/ReportsExample/Controllers/ReportsController.cs @@ -1,8 +1,8 @@ using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; -using JsonApiDotNetCore.Configuration; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using ReportsExample.Models; @@ -12,10 +12,10 @@ namespace ReportsExample.Controllers public class ReportsController : BaseJsonApiController { public ReportsController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IGetAllService getAll) - : base(jsonApiOptions, loggerFactory, getAll) + : base(options, loggerFactory, getAll) { } [HttpGet] diff --git a/src/Examples/ReportsExample/Models/Report.cs b/src/Examples/ReportsExample/Models/Report.cs index 66a5b21a48..8125f1f0ab 100644 --- a/src/Examples/ReportsExample/Models/Report.cs +++ b/src/Examples/ReportsExample/Models/Report.cs @@ -1,5 +1,5 @@ -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace ReportsExample.Models { diff --git a/src/Examples/ReportsExample/Startup.cs b/src/Examples/ReportsExample/Startup.cs index 7f01ea28db..fcb5274e8f 100644 --- a/src/Examples/ReportsExample/Startup.cs +++ b/src/Examples/ReportsExample/Startup.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore; +using JsonApiDotNetCore.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; diff --git a/src/JsonApiDotNetCore/AssemblyInfo.cs b/src/JsonApiDotNetCore/AssemblyInfo.cs deleted file mode 100644 index 6fa08b113d..0000000000 --- a/src/JsonApiDotNetCore/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System.Runtime.CompilerServices; -[assembly:InternalsVisibleTo("UnitTests")] -[assembly:InternalsVisibleTo("JsonApiDotNetCoreExampleTests")] -[assembly:InternalsVisibleTo("NoEntityFrameworkTests")] -[assembly:InternalsVisibleTo("Benchmarks")] -[assembly:InternalsVisibleTo("ResourceEntitySeparationExampleTests")] diff --git a/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs deleted file mode 100644 index 143c05650d..0000000000 --- a/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCore.Builders -{ - public interface IResourceGraphBuilder - { - /// - /// Construct the - /// - IResourceGraph Build(); - /// - /// Add a json:api resource - /// - /// The resource model type - /// - /// The pluralized name that should be exposed by the API. - /// If nothing is specified, the configured name formatter will be used. - /// - IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable; - /// - /// Add a json:api resource - /// - /// The resource model type - /// The resource model identifier type - /// - /// The pluralized name that should be exposed by the API. - /// If nothing is specified, the configured name formatter will be used. - /// - IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable; - /// - /// Add a Json:Api resource - /// - /// The resource model type - /// The resource model identifier type - /// - /// The pluralized name that should be exposed by the API. - /// If nothing is specified, the configured name formatter will be used. - /// - IResourceGraphBuilder AddResource(Type resourceType, Type idType = null, string pluralizedTypeName = null); - } -} diff --git a/src/JsonApiDotNetCore/Extensions/ApplicationBuilderExtensions.cs b/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs similarity index 86% rename from src/JsonApiDotNetCore/Extensions/ApplicationBuilderExtensions.cs rename to src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs index 8d943101e0..ebff426d1a 100644 --- a/src/JsonApiDotNetCore/Extensions/ApplicationBuilderExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs @@ -1,7 +1,8 @@ +using System; using JsonApiDotNetCore.Middleware; using Microsoft.AspNetCore.Builder; -namespace JsonApiDotNetCore +namespace JsonApiDotNetCore.Configuration { public static class ApplicationBuilderExtensions { @@ -20,6 +21,8 @@ public static class ApplicationBuilderExtensions /// public static void UseJsonApi(this IApplicationBuilder builder) { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + builder.UseMiddleware(); } } diff --git a/src/JsonApiDotNetCore/Configuration/GenericServiceFactory.cs b/src/JsonApiDotNetCore/Configuration/GenericServiceFactory.cs new file mode 100644 index 0000000000..d421102c08 --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/GenericServiceFactory.cs @@ -0,0 +1,41 @@ +using System; + +namespace JsonApiDotNetCore.Configuration +{ + /// + public sealed class GenericServiceFactory : IGenericServiceFactory + { + private readonly IServiceProvider _serviceProvider; + + public GenericServiceFactory(IRequestScopedServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + } + + /// + public TInterface Get(Type openGenericType, Type resourceType) + { + if (openGenericType == null) throw new ArgumentNullException(nameof(openGenericType)); + if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + + return GetInternal(openGenericType, resourceType); + } + + /// + public TInterface Get(Type openGenericType, Type resourceType, Type keyType) + { + if (openGenericType == null) throw new ArgumentNullException(nameof(openGenericType)); + if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + if (keyType == null) throw new ArgumentNullException(nameof(keyType)); + + return GetInternal(openGenericType, resourceType, keyType); + } + + private TInterface GetInternal(Type openGenericType, params Type[] types) + { + var concreteType = openGenericType.MakeGenericType(types); + + return (TInterface)_serviceProvider.GetService(concreteType); + } + } +} diff --git a/src/JsonApiDotNetCore/Configuration/IGenericServiceFactory.cs b/src/JsonApiDotNetCore/Configuration/IGenericServiceFactory.cs new file mode 100644 index 0000000000..70ac627218 --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/IGenericServiceFactory.cs @@ -0,0 +1,31 @@ +using System; + +namespace JsonApiDotNetCore.Configuration +{ + /// + /// Represents the Service Locator design pattern. Used to obtain object instances for types are not known until runtime. + /// The typical use case would be for accessing relationship data or resolving operations processors. + /// + public interface IGenericServiceFactory + { + /// + /// Constructs the generic type and locates the service, then casts to . + /// + /// + /// (typeof(GenericProcessor<>), typeof(TResource)); + /// ]]> + /// + TInterface Get(Type openGenericType, Type resourceType); + + /// + /// Constructs the generic type and locates the service, then casts to . + /// + /// + /// (typeof(GenericProcessor<>), typeof(TResource), typeof(TId)); + /// ]]> + /// + TInterface Get(Type openGenericType, Type resourceType, Type keyType); + } +} diff --git a/src/JsonApiDotNetCore/Configuration/IInverseRelationships.cs b/src/JsonApiDotNetCore/Configuration/IInverseRelationships.cs new file mode 100644 index 0000000000..b15afea2ce --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/IInverseRelationships.cs @@ -0,0 +1,23 @@ +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCore.Configuration +{ + /// + /// Responsible for populating the property. + /// + /// This service is instantiated in the configure phase of the application. + /// + /// When using a data access layer different from EF Core, and when using ResourceHooks + /// that depend on the inverse navigation property (BeforeImplicitUpdateRelationship), + /// you will need to override this service, or pass along the inverseNavigationProperty in + /// the RelationshipAttribute. + /// + public interface IInverseRelationships + { + /// + /// This method is called upon startup by JsonApiDotNetCore. It should + /// deal with resolving the inverse relationships. + /// + void Resolve(); + } +} diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index 1f3750e38f..8c2455caf6 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -1,12 +1,14 @@ using System; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Configuration { + /// + /// Global options that configure the behavior of JsonApiDotNetCore. + /// public interface IJsonApiOptions { /// @@ -56,27 +58,27 @@ public interface IJsonApiOptions /// /// Configures globally which links to show in the /// object for a requested resource. Setting can be overridden per resource by - /// adding a to the class definition of that resource. + /// adding a to the class definition of that resource. /// - Links TopLevelLinks { get; } + LinkTypes TopLevelLinks { get; } /// /// Configures globally which links to show in the /// object for a requested resource. Setting can be overridden per resource by - /// adding a to the class definition of that resource. + /// adding a to the class definition of that resource. /// - Links ResourceLinks { get; } + LinkTypes ResourceLinks { get; } /// /// Configures globally which links to show in the /// object for a requested resource. Setting can be overridden per resource by - /// adding a to the class definition of that resource. + /// adding a to the class definition of that resource. /// This option can also be specified per relationship by using the associated links argument /// in the constructor of . /// /// /// - /// options.RelationshipLinks = Links.None; + /// options.RelationshipLinks = LinkTypes.None; /// /// /// { @@ -89,7 +91,7 @@ public interface IJsonApiOptions /// } /// /// - Links RelationshipLinks { get; } + LinkTypes RelationshipLinks { get; } /// /// Whether or not the total resource count should be included in all document-level meta objects. diff --git a/src/JsonApiDotNetCore/Configuration/IRelatedIdMapper.cs b/src/JsonApiDotNetCore/Configuration/IRelatedIdMapper.cs new file mode 100644 index 0000000000..71c813d608 --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/IRelatedIdMapper.cs @@ -0,0 +1,18 @@ +namespace JsonApiDotNetCore.Configuration +{ + /// + /// Provides an interface for formatting relationship identifiers from the navigation property name. + /// + public interface IRelatedIdMapper + { + /// + /// Gets the internal property name for the database mapped identifier property. + /// + /// + /// + /// RelatedIdMapper.GetRelatedIdPropertyName("Article"); // returns "ArticleId" + /// + /// + string GetRelatedIdPropertyName(string propertyName); + } +} diff --git a/src/JsonApiDotNetCore/Configuration/IRequestScopedServiceProvider.cs b/src/JsonApiDotNetCore/Configuration/IRequestScopedServiceProvider.cs new file mode 100644 index 0000000000..a3e72e1fb8 --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/IRequestScopedServiceProvider.cs @@ -0,0 +1,11 @@ +using System; + +namespace JsonApiDotNetCore.Configuration +{ + /// + /// An interface used to separate the registration of the global + /// from a request-scoped service provider. This is useful in cases when we need to + /// manually resolve services from the request scope (e.g. operation processors). + /// + public interface IRequestScopedServiceProvider : IServiceProvider { } +} diff --git a/src/JsonApiDotNetCore/Internal/Contracts/IResourceContextProvider.cs b/src/JsonApiDotNetCore/Configuration/IResourceContextProvider.cs similarity index 65% rename from src/JsonApiDotNetCore/Internal/Contracts/IResourceContextProvider.cs rename to src/JsonApiDotNetCore/Configuration/IResourceContextProvider.cs index 6956aabc05..a03b40870d 100644 --- a/src/JsonApiDotNetCore/Internal/Contracts/IResourceContextProvider.cs +++ b/src/JsonApiDotNetCore/Configuration/IResourceContextProvider.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Resources; -namespace JsonApiDotNetCore.Internal.Contracts +namespace JsonApiDotNetCore.Configuration { /// /// Responsible for getting s from the . @@ -12,20 +12,20 @@ public interface IResourceContextProvider /// /// Gets all registered resource contexts. /// - IEnumerable GetResourceContexts(); + IReadOnlyCollection GetResourceContexts(); /// - /// Get the resource metadata by the DbSet property name + /// Gets the resource metadata for the specified exposed resource name. /// ResourceContext GetResourceContext(string resourceName); /// - /// Get the resource metadata by the resource type + /// Gets the resource metadata for the specified resource type. /// ResourceContext GetResourceContext(Type resourceType); /// - /// Get the resource metadata by the resource type + /// Gets the resource metadata for the specified resource type. /// ResourceContext GetResourceContext() where TResource : class, IIdentifiable; } diff --git a/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs new file mode 100644 index 0000000000..8a11d587f1 --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCore.Configuration +{ + /// + /// Enables retrieving the exposed resource fields (attributes and relationships) of resources registered in the resource graph. + /// + public interface IResourceGraph : IResourceContextProvider + { + /// + /// Gets all fields (attributes and relationships) for + /// that are targeted by the selector. If no selector is provided, all + /// exposed fields are returned. + /// + /// The resource for which to retrieve fields. + /// Should be of the form: (TResource e) => new { e.Field1, e.Field2 } + IReadOnlyCollection GetFields(Expression> selector = null) where TResource : class, IIdentifiable; + + /// + /// Gets all attributes for + /// that are targeted by the selector. If no selector is provided, all + /// exposed fields are returned. + /// + /// The resource for which to retrieve attributes. + /// Should be of the form: (TResource e) => new { e.Attribute1, e.Attribute2 } + IReadOnlyCollection GetAttributes(Expression> selector = null) where TResource : class, IIdentifiable; + + /// + /// Gets all relationships for + /// that are targeted by the selector. If no selector is provided, all + /// exposed fields are returned. + /// + /// The resource for which to retrieve relationships. + /// Should be of the form: (TResource e) => new { e.Relationship1, e.Relationship2 } + IReadOnlyCollection GetRelationships(Expression> selector = null) where TResource : class, IIdentifiable; + + /// + /// Gets all exposed fields (attributes and relationships) for the specified type. + /// + /// The resource type. Must implement . + IReadOnlyCollection GetFields(Type type); + + /// + /// Gets all exposed attributes for the specified type. + /// + /// The resource type. Must implement . + IReadOnlyCollection GetAttributes(Type type); + + /// + /// Gets all exposed relationships for the specified type. + /// + /// The resource type. Must implement . + IReadOnlyCollection GetRelationships(Type type); + + /// + /// Traverses the resource graph, looking for the inverse relationship of the specified + /// . + /// + RelationshipAttribute GetInverseRelationship(RelationshipAttribute relationship); + } +} diff --git a/src/JsonApiDotNetCore/Configuration/IResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Configuration/IResourceGraphBuilder.cs new file mode 100644 index 0000000000..705bade6e9 --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/IResourceGraphBuilder.cs @@ -0,0 +1,42 @@ +using System; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Configuration +{ + public interface IResourceGraphBuilder + { + /// + /// Constructs the . + /// + IResourceGraph Build(); + /// + /// Adds a json:api resource. + /// + /// The resource model type. + /// + /// The pluralized name, under which the resource is publicly exposed by the API. + /// If nothing is specified, the configured casing convention formatter will be applied. + /// + IResourceGraphBuilder Add(string pluralizedTypeName = null) where TResource : class, IIdentifiable; + /// + /// Adds a json:api resource. + /// + /// The resource model type. + /// The resource model identifier type. + /// + /// The pluralized name, under which the resource is publicly exposed by the API. + /// If nothing is specified, the configured casing convention formatter will be applied. + /// + IResourceGraphBuilder Add(string pluralizedTypeName = null) where TResource : class, IIdentifiable; + /// + /// Adds a json:api resource. + /// + /// The resource model type. + /// The resource model identifier type. + /// + /// The pluralized name, under which the resource is publicly exposed by the API. + /// If nothing is specified, the configured casing convention formatter will be applied. + /// + IResourceGraphBuilder Add(Type resourceType, Type idType = null, string pluralizedTypeName = null); + } +} diff --git a/src/JsonApiDotNetCore/Configuration/IServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Configuration/IServiceDiscoveryFacade.cs new file mode 100644 index 0000000000..4e952d9f82 --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/IServiceDiscoveryFacade.cs @@ -0,0 +1,20 @@ +using System.Reflection; + +namespace JsonApiDotNetCore.Configuration +{ + /// + /// Scans for types like resources, services, repositories and resource definitions in an assembly and registers them to the IoC container. This is part of the resource auto-discovery process. + /// + public interface IServiceDiscoveryFacade + { + /// + /// Scans in the specified assembly. + /// + ServiceDiscoveryFacade AddAssembly(Assembly assembly); + + /// + /// Scans in the calling assembly. + /// + ServiceDiscoveryFacade AddCurrentAssembly(); + } +} diff --git a/src/JsonApiDotNetCore/Graph/IdentifiableTypeCache.cs b/src/JsonApiDotNetCore/Configuration/IdentifiableTypeCache.cs similarity index 56% rename from src/JsonApiDotNetCore/Graph/IdentifiableTypeCache.cs rename to src/JsonApiDotNetCore/Configuration/IdentifiableTypeCache.cs index cc45386e9e..f0c5b309e8 100644 --- a/src/JsonApiDotNetCore/Graph/IdentifiableTypeCache.cs +++ b/src/JsonApiDotNetCore/Configuration/IdentifiableTypeCache.cs @@ -2,23 +2,23 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Resources; -namespace JsonApiDotNetCore.Graph +namespace JsonApiDotNetCore.Configuration { /// - /// Used to cache and locate types, to facilitate auto-resource discovery + /// Used to cache and locate types, to facilitate resource auto-discovery. /// internal sealed class IdentifiableTypeCache { - private readonly ConcurrentDictionary> _typeCache = new ConcurrentDictionary>(); + private readonly ConcurrentDictionary> _typeCache = new ConcurrentDictionary>(); /// - /// Get all implementations of in the assembly + /// Gets all implementations of in the assembly. /// - public IEnumerable GetIdentifiableTypes(Assembly assembly) + public IReadOnlyCollection GetIdentifiableTypes(Assembly assembly) { - return _typeCache.GetOrAdd(assembly, asm => FindIdentifiableTypes(asm).ToList()); + return _typeCache.GetOrAdd(assembly, asm => FindIdentifiableTypes(asm).ToArray()); } private static IEnumerable FindIdentifiableTypes(Assembly assembly) diff --git a/src/JsonApiDotNetCore/Internal/InverseRelationships.cs b/src/JsonApiDotNetCore/Configuration/InverseRelationships.cs similarity index 52% rename from src/JsonApiDotNetCore/Internal/InverseRelationships.cs rename to src/JsonApiDotNetCore/Configuration/InverseRelationships.cs index 49b36053e6..5c217373dd 100644 --- a/src/JsonApiDotNetCore/Internal/InverseRelationships.cs +++ b/src/JsonApiDotNetCore/Configuration/InverseRelationships.cs @@ -1,31 +1,11 @@ -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models.Annotation; +using System; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Resources.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; -namespace JsonApiDotNetCore.Internal +namespace JsonApiDotNetCore.Configuration { - /// - /// Responsible for populating the RelationshipAttribute InverseNavigation property. - /// - /// This service is instantiated in the configure phase of the application. - /// - /// When using a data access layer different from EF Core, and when using ResourceHooks - /// that depend on the inverse navigation property (BeforeImplicitUpdateRelationship), - /// you will need to override this service, or pass along the inverseNavigationProperty in - /// the RelationshipAttribute. - /// - public interface IInverseRelationships - { - /// - /// This method is called upon startup by JsonApiDotNetCore. It should - /// deal with resolving the inverse relationships. - /// - void Resolve(); - - } - /// public class InverseRelationships : IInverseRelationships { @@ -34,7 +14,7 @@ public class InverseRelationships : IInverseRelationships public InverseRelationships(IResourceContextProvider provider, IDbContextResolver resolver = null) { - _provider = provider; + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); _resolver = resolver; } @@ -59,10 +39,7 @@ public void Resolve() } } - /// - /// If EF Core is not being used, we're expecting the resolver to not be registered. - /// - /// true, if Entity Framework Core was enabled, false otherwise. + // If EF Core is not being used, we're expecting the resolver to not be registered. private bool IsEntityFrameworkCoreEnabled() => _resolver != null; } } diff --git a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs similarity index 91% rename from src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs rename to src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 6102e9b851..87222097e0 100644 --- a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -1,31 +1,24 @@ using System; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Formatters; -using JsonApiDotNetCore.Graph; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Generics; +using JsonApiDotNetCore.Hooks.Internal; +using JsonApiDotNetCore.Hooks.Internal.Discovery; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Hooks.Internal.Traversal; using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Queries.Internal; +using JsonApiDotNetCore.QueryStrings; +using JsonApiDotNetCore.QueryStrings.Internal; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Hooks; +using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Internal.Queries; -using JsonApiDotNetCore.Internal.QueryStrings; -using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.QueryStrings; -using JsonApiDotNetCore.Serialization.Server.Builders; -using JsonApiDotNetCore.Serialization.Server; using Microsoft.Extensions.DependencyInjection.Extensions; -using JsonApiDotNetCore.RequestServices; -using JsonApiDotNetCore.RequestServices.Contracts; -using JsonApiDotNetCore.Services.Contract; -namespace JsonApiDotNetCore.Builders +namespace JsonApiDotNetCore.Configuration { /// /// A utility class that builds a JsonApi application. It registers all required services @@ -42,12 +35,12 @@ internal sealed class JsonApiApplicationBuilder public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mvcBuilder) { - _services = services; - _mvcBuilder = mvcBuilder; + _services = services ?? throw new ArgumentNullException(nameof(services)); + _mvcBuilder = mvcBuilder ?? throw new ArgumentNullException(nameof(mvcBuilder)); } /// - /// Executes the action provided by the user to configure + /// Executes the action provided by the user to configure . /// public void ConfigureJsonApiOptions(Action options) { @@ -55,7 +48,7 @@ public void ConfigureJsonApiOptions(Action options) } /// - /// Configures built-in .NET Core MVC (things like middleware, routing). Most of this configuration can be adjusted for the developers' need. + /// Configures built-in ASP.NET Core MVC (things like middleware, routing). Most of this configuration can be adjusted for the developers' need. /// Before calling .AddJsonApi(), a developer can register their own implementation of the following services to customize startup: /// , , , /// and . @@ -108,7 +101,7 @@ private void AddResourceTypesFromDbContext(ServiceProvider intermediateProvider) foreach (var entityType in dbContext.Model.GetEntityTypes()) { - _resourceGraphBuilder.AddResource(entityType.ClrType); + _resourceGraphBuilder.Add(entityType.ClrType); } } } @@ -122,7 +115,7 @@ public void AutoDiscover(Action autoDiscover) } /// - /// Executes the action provided by the user to configure the resources using + /// Executes the action provided by the user to configure the resources using . /// public void ConfigureResources(Action resources) { @@ -185,8 +178,8 @@ public void ConfigureServices() _services.AddSingleton(resourceGraph); _services.AddSingleton(); - _services.AddScoped(); - _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 5588026bff..b62e86f0af 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -1,76 +1,73 @@ -using JsonApiDotNetCore.Graph; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Resources.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Configuration { - /// - /// Global options - /// - public class JsonApiOptions : IJsonApiOptions + /// + public sealed class JsonApiOptions : IJsonApiOptions { - /// + /// public string Namespace { get; set; } - /// + /// public AttrCapabilities DefaultAttrCapabilities { get; set; } = AttrCapabilities.All; - /// + /// public bool IncludeExceptionStackTraceInErrors { get; set; } - /// + /// public bool UseRelativeLinks { get; set; } - /// - public Links TopLevelLinks { get; set; } = Links.All; + /// + public LinkTypes TopLevelLinks { get; set; } = LinkTypes.All; - /// - public Links ResourceLinks { get; set; } = Links.All; + /// + public LinkTypes ResourceLinks { get; set; } = LinkTypes.All; - /// - public Links RelationshipLinks { get; set; } = Links.All; + /// + public LinkTypes RelationshipLinks { get; set; } = LinkTypes.All; - /// + /// public bool IncludeTotalResourceCount { get; set; } - /// + /// public PageSize DefaultPageSize { get; set; } = new PageSize(10); - /// + /// public PageSize MaximumPageSize { get; set; } - /// + /// public PageNumber MaximumPageNumber { get; set; } - /// + /// public bool ValidateModelState { get; set; } - /// + /// public bool AllowClientGeneratedIds { get; set; } - /// + /// public bool EnableResourceHooks { get; set; } - /// + /// public bool LoadDatabaseValues { get; set; } - /// + /// public bool AllowUnknownQueryStringParameters { get; set; } - /// + /// public bool EnableLegacyFilterNotation { get; set; } - /// + /// public bool AllowQueryStringOverrideForSerializerNullValueHandling { get; set; } - /// + /// public bool AllowQueryStringOverrideForSerializerDefaultValueHandling { get; set; } + /// public int? MaximumIncludeDepth { get; set; } - /// + /// public JsonSerializerSettings SerializerSettings { get; } = new JsonSerializerSettings { ContractResolver = new DefaultContractResolver @@ -80,7 +77,7 @@ public class JsonApiOptions : IJsonApiOptions }; /// - /// Provides an interface for formatting relationship id properties given the navigation property name + /// Provides an interface for formatting relationship ID properties given the navigation property name. /// public static IRelatedIdMapper RelatedIdMapper { get; set; } = new RelatedIdMapper(); diff --git a/src/JsonApiDotNetCore/PageNumber.cs b/src/JsonApiDotNetCore/Configuration/PageNumber.cs similarity index 96% rename from src/JsonApiDotNetCore/PageNumber.cs rename to src/JsonApiDotNetCore/Configuration/PageNumber.cs index fc45910efe..bc94b3ca75 100644 --- a/src/JsonApiDotNetCore/PageNumber.cs +++ b/src/JsonApiDotNetCore/Configuration/PageNumber.cs @@ -1,6 +1,6 @@ using System; -namespace JsonApiDotNetCore +namespace JsonApiDotNetCore.Configuration { public sealed class PageNumber : IEquatable { diff --git a/src/JsonApiDotNetCore/PageSize.cs b/src/JsonApiDotNetCore/Configuration/PageSize.cs similarity index 95% rename from src/JsonApiDotNetCore/PageSize.cs rename to src/JsonApiDotNetCore/Configuration/PageSize.cs index 56cf9296fc..c1e3de877c 100644 --- a/src/JsonApiDotNetCore/PageSize.cs +++ b/src/JsonApiDotNetCore/Configuration/PageSize.cs @@ -1,6 +1,6 @@ using System; -namespace JsonApiDotNetCore +namespace JsonApiDotNetCore.Configuration { public sealed class PageSize : IEquatable { diff --git a/src/JsonApiDotNetCore/Configuration/RelatedIdMapper.cs b/src/JsonApiDotNetCore/Configuration/RelatedIdMapper.cs new file mode 100644 index 0000000000..0d238aeb5b --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/RelatedIdMapper.cs @@ -0,0 +1,9 @@ +namespace JsonApiDotNetCore.Configuration +{ + /// + public sealed class RelatedIdMapper : IRelatedIdMapper + { + /// + public string GetRelatedIdPropertyName(string propertyName) => propertyName + "Id"; + } +} diff --git a/src/JsonApiDotNetCore/Services/ScopedServiceProvider.cs b/src/JsonApiDotNetCore/Configuration/RequestScopedServiceProvider.cs similarity index 55% rename from src/JsonApiDotNetCore/Services/ScopedServiceProvider.cs rename to src/JsonApiDotNetCore/Configuration/RequestScopedServiceProvider.cs index 65ea08df58..1601e08c73 100644 --- a/src/JsonApiDotNetCore/Services/ScopedServiceProvider.cs +++ b/src/JsonApiDotNetCore/Configuration/RequestScopedServiceProvider.cs @@ -1,36 +1,29 @@ -using Microsoft.AspNetCore.Http; using System; +using Microsoft.AspNetCore.Http; -namespace JsonApiDotNetCore.Services +namespace JsonApiDotNetCore.Configuration { - /// - /// An interface used to separate the registration of the global ServiceProvider - /// from a request scoped service provider. This is useful in cases when we need to - /// manually resolve services from the request scope (e.g. operation processors) - /// - public interface IScopedServiceProvider : IServiceProvider { } - - /// - /// A service provider that uses the current HttpContext request scope - /// - public sealed class RequestScopedServiceProvider : IScopedServiceProvider + /// + public sealed class RequestScopedServiceProvider : IRequestScopedServiceProvider { private readonly IHttpContextAccessor _httpContextAccessor; public RequestScopedServiceProvider(IHttpContextAccessor httpContextAccessor) { - _httpContextAccessor = httpContextAccessor; + _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } /// public object GetService(Type serviceType) { + if (serviceType == null) throw new ArgumentNullException(nameof(serviceType)); + if (_httpContextAccessor.HttpContext == null) { throw new InvalidOperationException( $"Cannot resolve scoped service '{serviceType.FullName}' outside the context of an HTTP request. " + "If you are hitting this error in automated tests, you should instead inject your own " + - "IScopedServiceProvider implementation. See the GitHub repository for how we do this internally. " + + "IRequestScopedServiceProvider implementation. See the GitHub repository for how we do this internally. " + "https://github.com/json-api-dotnet/JsonApiDotNetCore/search?q=TestScopedServiceProvider&unscoped_q=TestScopedServiceProvider"); } diff --git a/src/JsonApiDotNetCore/Configuration/ResourceContext.cs b/src/JsonApiDotNetCore/Configuration/ResourceContext.cs new file mode 100644 index 0000000000..eb5af30a56 --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/ResourceContext.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCore.Configuration +{ + /// + /// Provides metadata for a resource, such as its attributes and relationships. + /// + public class ResourceContext + { + /// + /// The publicly exposed resource name. + /// + public string ResourceName { get; set; } + + /// + /// The CLR type of the resource. + /// + public Type ResourceType { get; set; } + + /// + /// The identity type of the resource. + /// + public Type IdentityType { get; set; } + + /// + /// The concrete type. + /// We store this so that we don't need to re-compute the generic type. + /// + public Type ResourceDefinitionType { get; set; } + + /// + /// Exposed resource attributes. + /// See https://jsonapi.org/format/#document-resource-object-attributes. + /// + public IReadOnlyCollection Attributes { get; set; } + + /// + /// Exposed resource relationships. + /// See https://jsonapi.org/format/#document-resource-object-relationships. + /// + public IReadOnlyCollection Relationships { get; set; } + + /// + /// Related entities that are not exposed as resource relationships. + /// + public IReadOnlyCollection EagerLoads { get; set; } + + private IReadOnlyCollection _fields; + + /// + /// Exposed resource attributes and relationships. + /// See https://jsonapi.org/format/#document-resource-object-fields. + /// + public IReadOnlyCollection Fields => _fields ??= Attributes.Cast().Concat(Relationships).ToArray(); + + /// + /// Configures which links to show in the + /// object for this resource. If set to , + /// the configuration will be read from . + /// Defaults to . + /// + public LinkTypes TopLevelLinks { get; internal set; } = LinkTypes.NotConfigured; + + /// + /// Configures which links to show in the + /// object for this resource. If set to , + /// the configuration will be read from . + /// Defaults to . + /// + public LinkTypes ResourceLinks { get; internal set; } = LinkTypes.NotConfigured; + + /// + /// Configures which links to show in the + /// for all relationships of the resource for which this attribute was instantiated. + /// If set to , the configuration will + /// be read from or + /// . Defaults to . + /// + public LinkTypes RelationshipLinks { get; internal set; } = LinkTypes.NotConfigured; + + public override string ToString() + { + return ResourceName; + } + } +} diff --git a/src/JsonApiDotNetCore/Graph/ResourceDescriptor.cs b/src/JsonApiDotNetCore/Configuration/ResourceDescriptor.cs similarity index 81% rename from src/JsonApiDotNetCore/Graph/ResourceDescriptor.cs rename to src/JsonApiDotNetCore/Configuration/ResourceDescriptor.cs index 99da83ab25..a038930b65 100644 --- a/src/JsonApiDotNetCore/Graph/ResourceDescriptor.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceDescriptor.cs @@ -1,18 +1,18 @@ using System; -namespace JsonApiDotNetCore.Graph +namespace JsonApiDotNetCore.Configuration { - internal struct ResourceDescriptor + internal class ResourceDescriptor { + public Type ResourceType { get; } + public Type IdType { get; } + + internal static readonly ResourceDescriptor Empty = new ResourceDescriptor(null, null); + public ResourceDescriptor(Type resourceType, Type idType) { ResourceType = resourceType; IdType = idType; } - - public Type ResourceType { get; } - public Type IdType { get; } - - internal static readonly ResourceDescriptor Empty = new ResourceDescriptor(null, null); } } diff --git a/src/JsonApiDotNetCore/Internal/ResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs similarity index 52% rename from src/JsonApiDotNetCore/Internal/ResourceGraph.cs rename to src/JsonApiDotNetCore/Configuration/ResourceGraph.cs index 0ae16db8b1..f8367af7f5 100644 --- a/src/JsonApiDotNetCore/Internal/ResourceGraph.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs @@ -2,86 +2,109 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Internal +namespace JsonApiDotNetCore.Configuration { - /// - /// keeps track of all the models/resources defined in JADNC - /// + /// public class ResourceGraph : IResourceGraph { - private List Resources { get; } - private static readonly Type proxyTargetAccessorType = Type.GetType("Castle.DynamicProxy.IProxyTargetAccessor, Castle.Core"); + private readonly IReadOnlyCollection _resources; + private static readonly Type _proxyTargetAccessorType = Type.GetType("Castle.DynamicProxy.IProxyTargetAccessor, Castle.Core"); - public ResourceGraph(List resources) + public ResourceGraph(IReadOnlyCollection resources) { - Resources = resources; + _resources = resources ?? throw new ArgumentNullException(nameof(resources)); } /// - public IEnumerable GetResourceContexts() => Resources; + public IReadOnlyCollection GetResourceContexts() => _resources; + /// public ResourceContext GetResourceContext(string resourceName) - => Resources.SingleOrDefault(e => e.ResourceName == resourceName); + { + if (resourceName == null) throw new ArgumentNullException(nameof(resourceName)); + + return _resources.SingleOrDefault(e => e.ResourceName == resourceName); + } + /// public ResourceContext GetResourceContext(Type resourceType) - => IsLazyLoadingProxyForResourceType(resourceType) ? - Resources.SingleOrDefault(e => e.ResourceType == resourceType.BaseType) : - Resources.SingleOrDefault(e => e.ResourceType == resourceType); + { + if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + + return IsLazyLoadingProxyForResourceType(resourceType) + ? _resources.SingleOrDefault(e => e.ResourceType == resourceType.BaseType) + : _resources.SingleOrDefault(e => e.ResourceType == resourceType); + } + /// public ResourceContext GetResourceContext() where TResource : class, IIdentifiable => GetResourceContext(typeof(TResource)); - /// - public List GetFields(Expression> selector = null) where T : IIdentifiable + + /// + public IReadOnlyCollection GetFields(Expression> selector = null) where TResource : class, IIdentifiable { - return Getter(selector).ToList(); + return Getter(selector); } - /// - public List GetAttributes(Expression> selector = null) where T : IIdentifiable + + /// + public IReadOnlyCollection GetAttributes(Expression> selector = null) where TResource : class, IIdentifiable { - return Getter(selector, FieldFilterType.Attribute).Cast().ToList(); + return Getter(selector, FieldFilterType.Attribute).Cast().ToArray(); } - /// - public List GetRelationships(Expression> selector = null) where T : IIdentifiable + + /// + public IReadOnlyCollection GetRelationships(Expression> selector = null) where TResource : class, IIdentifiable { - return Getter(selector, FieldFilterType.Relationship).Cast().ToList(); + return Getter(selector, FieldFilterType.Relationship).Cast().ToArray(); } - /// - public List GetFields(Type type) + + /// + public IReadOnlyCollection GetFields(Type type) { - return GetResourceContext(type).Fields.ToList(); + if (type == null) throw new ArgumentNullException(nameof(type)); + + return GetResourceContext(type).Fields; } - /// - public List GetAttributes(Type type) + + /// + public IReadOnlyCollection GetAttributes(Type type) { - return GetResourceContext(type).Attributes.ToList(); + if (type == null) throw new ArgumentNullException(nameof(type)); + + return GetResourceContext(type).Attributes; } - /// - public List GetRelationships(Type type) + + /// + public IReadOnlyCollection GetRelationships(Type type) { - return GetResourceContext(type).Relationships.ToList(); + if (type == null) throw new ArgumentNullException(nameof(type)); + + return GetResourceContext(type).Relationships; } + /// - public RelationshipAttribute GetInverse(RelationshipAttribute relationship) + public RelationshipAttribute GetInverseRelationship(RelationshipAttribute relationship) { + if (relationship == null) throw new ArgumentNullException(nameof(relationship)); + if (relationship.InverseNavigation == null) return null; return GetResourceContext(relationship.RightType) .Relationships .SingleOrDefault(r => r.Property.Name == relationship.InverseNavigation); } - private IEnumerable Getter(Expression> selector = null, FieldFilterType type = FieldFilterType.None) where T : IIdentifiable + private IReadOnlyCollection Getter(Expression> selector = null, FieldFilterType type = FieldFilterType.None) where TResource : class, IIdentifiable { - IEnumerable available; + IReadOnlyCollection available; if (type == FieldFilterType.Attribute) - available = GetResourceContext(typeof(T)).Attributes; + available = GetResourceContext(typeof(TResource)).Attributes; else if (type == FieldFilterType.Relationship) - available = GetResourceContext(typeof(T)).Relationships; + available = GetResourceContext(typeof(TResource)).Relationships; else - available = GetResourceContext(typeof(T)).Fields; + available = GetResourceContext(typeof(TResource)).Fields; if (selector == null) return available; @@ -132,7 +155,7 @@ private IEnumerable Getter(Expression - proxyTargetAccessorType?.IsAssignableFrom(resourceType) ?? false; + _proxyTargetAccessorType?.IsAssignableFrom(resourceType) ?? false; private static Expression RemoveConvert(Expression expression) => expression is UnaryExpression unaryExpression @@ -145,9 +168,6 @@ private void ThrowNotExposedError(string memberName, FieldFilterType type) throw new ArgumentException($"{memberName} is not an json:api exposed {type:g}."); } - /// - /// internally used only by . - /// private enum FieldFilterType { None, diff --git a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs similarity index 71% rename from src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs rename to src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs index f6781f93ed..50d635ea13 100644 --- a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs @@ -2,17 +2,14 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Graph; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; using Microsoft.Extensions.Logging; -namespace JsonApiDotNetCore.Builders +namespace JsonApiDotNetCore.Configuration { + /// public class ResourceGraphBuilder : IResourceGraphBuilder { private readonly IJsonApiOptions _options; @@ -21,7 +18,9 @@ public class ResourceGraphBuilder : IResourceGraphBuilder public ResourceGraphBuilder(IJsonApiOptions options, ILoggerFactory loggerFactory) { - _options = options; + if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); + + _options = options ?? throw new ArgumentNullException(nameof(options)); _logger = loggerFactory.CreateLogger(); } @@ -34,7 +33,7 @@ public IResourceGraph Build() private void SetResourceLinksOptions(ResourceContext resourceContext) { - var attribute = (LinksAttribute)resourceContext.ResourceType.GetCustomAttribute(typeof(LinksAttribute)); + var attribute = (ResourceLinksAttribute)resourceContext.ResourceType.GetCustomAttribute(typeof(ResourceLinksAttribute)); if (attribute != null) { resourceContext.RelationshipLinks = attribute.RelationshipLinks; @@ -44,19 +43,21 @@ private void SetResourceLinksOptions(ResourceContext resourceContext) } /// - public IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable - => AddResource(pluralizedTypeName); + public IResourceGraphBuilder Add(string pluralizedTypeName = null) where TResource : class, IIdentifiable + => Add(pluralizedTypeName); /// - public IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable - => AddResource(typeof(TResource), typeof(TId), pluralizedTypeName); + public IResourceGraphBuilder Add(string pluralizedTypeName = null) where TResource : class, IIdentifiable + => Add(typeof(TResource), typeof(TId), pluralizedTypeName); /// - public IResourceGraphBuilder AddResource(Type resourceType, Type idType = null, string pluralizedTypeName = null) + public IResourceGraphBuilder Add(Type resourceType, Type idType = null, string pluralizedTypeName = null) { + if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + if (_resources.All(e => e.ResourceType != resourceType)) { - if (resourceType.IsOrImplementsInterface(typeof(IIdentifiable))) + if (TypeHelper.IsOrImplementsInterface(resourceType, typeof(IIdentifiable))) { pluralizedTypeName ??= FormatResourceName(resourceType); idType ??= TypeLocator.GetIdType(resourceType); @@ -83,8 +84,10 @@ public IResourceGraphBuilder AddResource(Type resourceType, Type idType = null, ResourceDefinitionType = GetResourceDefinitionType(resourceType) }; - protected virtual List GetAttributes(Type resourceType) + protected virtual IReadOnlyCollection GetAttributes(Type resourceType) { + if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + var attributes = new List(); foreach (var property in resourceType.GetProperties()) @@ -92,7 +95,7 @@ protected virtual List GetAttributes(Type resourceType) var attribute = (AttrAttribute)property.GetCustomAttribute(typeof(AttrAttribute)); // Although strictly not correct, 'id' is added to the list of attributes for convenience. - // For example, it enables to filter on id, without the need to special-case existing logic. + // For example, it enables to filter on ID, without the need to special-case existing logic. // And when using sparse fields, it silently adds 'id' to the set of attributes to retrieve. if (property.Name == nameof(Identifiable.Id) && attribute == null) { @@ -122,8 +125,10 @@ protected virtual List GetAttributes(Type resourceType) return attributes; } - protected virtual List GetRelationships(Type resourceType) + protected virtual IReadOnlyCollection GetRelationships(Type resourceType) { + if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + var attributes = new List(); var properties = resourceType.GetProperties(); foreach (var prop in properties) @@ -141,11 +146,11 @@ protected virtual List GetRelationships(Type resourceType { var throughProperty = properties.SingleOrDefault(p => p.Name == hasManyThroughAttribute.ThroughPropertyName); if (throughProperty == null) - throw new JsonApiSetupException($"Invalid {nameof(HasManyThroughAttribute)} on '{resourceType}.{attribute.Property.Name}': Resource does not contain a property named '{hasManyThroughAttribute.ThroughPropertyName}'."); + throw new InvalidConfigurationException($"Invalid {nameof(HasManyThroughAttribute)} on '{resourceType}.{attribute.Property.Name}': Resource does not contain a property named '{hasManyThroughAttribute.ThroughPropertyName}'."); var throughType = TryGetThroughType(throughProperty); if (throughType == null) - throw new JsonApiSetupException($"Invalid {nameof(HasManyThroughAttribute)} on '{resourceType}.{attribute.Property.Name}': Referenced property '{throughProperty.Name}' does not implement 'ICollection'."); + throw new InvalidConfigurationException($"Invalid {nameof(HasManyThroughAttribute)} on '{resourceType}.{attribute.Property.Name}': Referenced property '{throughProperty.Name}' does not implement 'ICollection'."); // ICollection hasManyThroughAttribute.ThroughProperty = throughProperty; @@ -157,21 +162,21 @@ protected virtual List GetRelationships(Type resourceType // ArticleTag.Article hasManyThroughAttribute.LeftProperty = throughProperties.SingleOrDefault(x => x.PropertyType == resourceType) - ?? throw new JsonApiSetupException($"{throughType} does not contain a navigation property to type {resourceType}"); + ?? throw new InvalidConfigurationException($"{throughType} does not contain a navigation property to type {resourceType}"); // ArticleTag.ArticleId var leftIdPropertyName = JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(hasManyThroughAttribute.LeftProperty.Name); hasManyThroughAttribute.LeftIdProperty = throughProperties.SingleOrDefault(x => x.Name == leftIdPropertyName) - ?? throw new JsonApiSetupException($"{throughType} does not contain a relationship id property to type {resourceType} with name {leftIdPropertyName}"); + ?? throw new InvalidConfigurationException($"{throughType} does not contain a relationship ID property to type {resourceType} with name {leftIdPropertyName}"); // ArticleTag.Tag hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.RightType) - ?? throw new JsonApiSetupException($"{throughType} does not contain a navigation property to type {hasManyThroughAttribute.RightType}"); + ?? throw new InvalidConfigurationException($"{throughType} does not contain a navigation property to type {hasManyThroughAttribute.RightType}"); // ArticleTag.TagId var rightIdPropertyName = JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(hasManyThroughAttribute.RightProperty.Name); hasManyThroughAttribute.RightIdProperty = throughProperties.SingleOrDefault(x => x.Name == rightIdPropertyName) - ?? throw new JsonApiSetupException($"{throughType} does not contain a relationship id property to type {hasManyThroughAttribute.RightType} with name {rightIdPropertyName}"); + ?? throw new InvalidConfigurationException($"{throughType} does not contain a relationship ID property to type {hasManyThroughAttribute.RightType} with name {rightIdPropertyName}"); } } @@ -186,7 +191,7 @@ private static Type TryGetThroughType(PropertyInfo throughProperty) if (typeArguments.Length == 1) { var constructedThroughType = typeof(ICollection<>).MakeGenericType(typeArguments[0]); - if (throughProperty.PropertyType.IsOrImplementsInterface(constructedThroughType)) + if (TypeHelper.IsOrImplementsInterface(throughProperty.PropertyType, constructedThroughType)) { return typeArguments[0]; } @@ -196,10 +201,15 @@ private static Type TryGetThroughType(PropertyInfo throughProperty) return null; } - protected virtual Type GetRelationshipType(RelationshipAttribute relation, PropertyInfo prop) => - relation is HasOneAttribute ? prop.PropertyType : prop.PropertyType.GetGenericArguments()[0]; + protected virtual Type GetRelationshipType(RelationshipAttribute relationship, PropertyInfo property) + { + if (relationship == null) throw new ArgumentNullException(nameof(relationship)); + if (property == null) throw new ArgumentNullException(nameof(property)); + + return relationship is HasOneAttribute ? property.PropertyType : property.PropertyType.GetGenericArguments()[0]; + } - private List GetEagerLoads(Type resourceType, int recursionDepth = 0) + private IReadOnlyCollection GetEagerLoads(Type resourceType, int recursionDepth = 0) { if (recursionDepth >= 500) { diff --git a/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs b/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs new file mode 100644 index 0000000000..e58816186f --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs @@ -0,0 +1,28 @@ +using System; +using System.Reflection; +using Humanizer; +using JsonApiDotNetCore.Resources.Annotations; +using Newtonsoft.Json.Serialization; + +namespace JsonApiDotNetCore.Configuration +{ + internal sealed class ResourceNameFormatter + { + private readonly NamingStrategy _namingStrategy; + + public ResourceNameFormatter(IJsonApiOptions options) + { + _namingStrategy = options.SerializerContractResolver.NamingStrategy; + } + + /// + /// Gets the publicly visible resource name for the internal type name using the configured casing convention. + /// + public string FormatResourceName(Type resourceType) + { + return resourceType.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute + ? attribute.PublicName + : _namingStrategy.GetPropertyName(resourceType.Name.Pluralize(), false); + } + } +} diff --git a/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs similarity index 82% rename from src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs rename to src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs index 19863400ca..199937ed4c 100644 --- a/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs @@ -2,17 +2,13 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Graph; -using JsonApiDotNetCore.Internal; -using Microsoft.Extensions.DependencyInjection; -using JsonApiDotNetCore.Serialization.Client; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Serialization.Building; +using JsonApiDotNetCore.Serialization.Client.Internal; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; -namespace JsonApiDotNetCore +namespace JsonApiDotNetCore.Configuration { public static class ServiceCollectionExtensions { @@ -25,6 +21,8 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services, Action resources = null, IMvcCoreBuilder mvcBuilder = null) { + if (services == null) throw new ArgumentNullException(nameof(services)); + SetupApplicationBuilder(services, options, discovery, resources, mvcBuilder, null); ResolveInverseRelationships(services); @@ -41,6 +39,8 @@ public static IServiceCollection AddJsonApi(this IServiceCollection IMvcCoreBuilder mvcBuilder = null) where TDbContext : DbContext { + if (services == null) throw new ArgumentNullException(nameof(services)); + SetupApplicationBuilder(services, options, discovery, resources, mvcBuilder, typeof(TDbContext)); ResolveInverseRelationships(services); @@ -76,6 +76,8 @@ private static void ResolveInverseRelationships(IServiceCollection services) /// public static IServiceCollection AddClientSerialization(this IServiceCollection services) { + if (services == null) throw new ArgumentNullException(nameof(services)); + services.AddScoped(); services.AddScoped(sp => { @@ -86,14 +88,16 @@ public static IServiceCollection AddClientSerialization(this IServiceCollection } /// - /// Adds all required registrations for the service to the container + /// Adds all required registrations for the service to the container. /// - /// - public static IServiceCollection AddResourceService(this IServiceCollection services) + /// + public static IServiceCollection AddResourceService(this IServiceCollection services) { + if (services == null) throw new ArgumentNullException(nameof(services)); + var typeImplementsAnExpectedInterface = false; - var serviceImplementationType = typeof(T); + var serviceImplementationType = typeof(TService); // it is _possible_ that a single concrete type could be used for multiple resources... var resourceDescriptors = GetResourceTypesFromServiceImplementation(serviceImplementationType); @@ -102,11 +106,11 @@ public static IServiceCollection AddResourceService(this IServiceCollection s { foreach (var openGenericType in ServiceDiscoveryFacade.ServiceInterfaces) { - // A shorthand interface is one where the id type is omitted - // e.g. IResourceService is the shorthand for IResourceService + // A shorthand interface is one where the ID type is omitted + // e.g. IResourceService is the shorthand for IResourceService var isShorthandInterface = openGenericType.GetTypeInfo().GenericTypeParameters.Length == 1; if (isShorthandInterface && resourceDescriptor.IdType != typeof(int)) - continue; // we can't create a shorthand for id types other than int + continue; // we can't create a shorthand for ID types other than int var concreteGenericType = isShorthandInterface ? openGenericType.MakeGenericType(resourceDescriptor.ResourceType) @@ -120,8 +124,8 @@ public static IServiceCollection AddResourceService(this IServiceCollection s } } - if (typeImplementsAnExpectedInterface == false) - throw new JsonApiSetupException($"{serviceImplementationType} does not implement any of the expected JsonApiDotNetCore interfaces."); + if (!typeImplementsAnExpectedInterface) + throw new InvalidConfigurationException($"{serviceImplementationType} does not implement any of the expected JsonApiDotNetCore interfaces."); return services; } diff --git a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs similarity index 84% rename from src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs rename to src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs index cbe795cdce..d20c4dcc44 100644 --- a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs @@ -1,17 +1,17 @@ -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; -namespace JsonApiDotNetCore.Graph +namespace JsonApiDotNetCore.Configuration { + /// public class ServiceDiscoveryFacade : IServiceDiscoveryFacade { internal static readonly HashSet ServiceInterfaces = new HashSet { @@ -37,7 +37,7 @@ public class ServiceDiscoveryFacade : IServiceDiscoveryFacade typeof(IDeleteService<,>) }; - private static readonly HashSet RepositoryInterfaces = new HashSet { + private static readonly HashSet _repositoryInterfaces = new HashSet { typeof(IResourceRepository<>), typeof(IResourceRepository<,>), typeof(IResourceWriteRepository<>), @@ -52,20 +52,18 @@ public class ServiceDiscoveryFacade : IServiceDiscoveryFacade public ServiceDiscoveryFacade(IServiceCollection services, IResourceGraphBuilder resourceGraphBuilder) { - _services = services; - _resourceGraphBuilder = resourceGraphBuilder; + _services = services ?? throw new ArgumentNullException(nameof(services)); + _resourceGraphBuilder = resourceGraphBuilder ?? throw new ArgumentNullException(nameof(resourceGraphBuilder)); } - /// - /// Adds resource, service and repository implementations to the container. - /// + /// public ServiceDiscoveryFacade AddCurrentAssembly() => AddAssembly(Assembly.GetCallingAssembly()); - /// - /// Adds resource, service and repository implementations defined in the specified assembly to the container. - /// + /// public ServiceDiscoveryFacade AddAssembly(Assembly assembly) { + if (assembly == null) throw new ArgumentNullException(nameof(assembly)); + AddDbContextResolvers(assembly); var resourceDescriptors = _typeCache.GetIdentifiableTypes(assembly); @@ -92,7 +90,7 @@ private void AddResource(Assembly assembly, ResourceDescriptor resourceDescripto { RegisterResourceDefinition(assembly, resourceDescriptor); - _resourceGraphBuilder.AddResource(resourceDescriptor.ResourceType, resourceDescriptor.IdType); + _resourceGraphBuilder.Add(resourceDescriptor.ResourceType, resourceDescriptor.IdType); } private void RegisterResourceDefinition(Assembly assembly, ResourceDescriptor identifiable) @@ -107,7 +105,7 @@ private void RegisterResourceDefinition(Assembly assembly, ResourceDescriptor id } catch (InvalidOperationException e) { - throw new JsonApiSetupException($"Cannot define multiple ResourceDefinition<> implementations for '{identifiable.ResourceType}'", e); + throw new InvalidConfigurationException($"Cannot define multiple ResourceDefinition<> implementations for '{identifiable.ResourceType}'", e); } } @@ -121,7 +119,7 @@ private void AddServices(Assembly assembly, ResourceDescriptor resourceDescripto private void AddRepositories(Assembly assembly, ResourceDescriptor resourceDescriptor) { - foreach (var serviceInterface in RepositoryInterfaces) + foreach (var serviceInterface in _repositoryInterfaces) { RegisterServiceImplementations(assembly, serviceInterface, resourceDescriptor); } diff --git a/src/JsonApiDotNetCore/Graph/TypeLocator.cs b/src/JsonApiDotNetCore/Configuration/TypeLocator.cs similarity index 73% rename from src/JsonApiDotNetCore/Graph/TypeLocator.cs rename to src/JsonApiDotNetCore/Configuration/TypeLocator.cs index cd7470b8ba..4661f19dd0 100644 --- a/src/JsonApiDotNetCore/Graph/TypeLocator.cs +++ b/src/JsonApiDotNetCore/Configuration/TypeLocator.cs @@ -1,19 +1,18 @@ -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Models; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using JsonApiDotNetCore.Resources; -namespace JsonApiDotNetCore.Graph +namespace JsonApiDotNetCore.Configuration { /// - /// Used to locate types and facilitate auto-resource discovery + /// Used to locate types and facilitate resource auto-discovery. /// internal static class TypeLocator { /// - /// Determine whether or not this is a json:api resource by checking if it implements . + /// Determine whether or not this is a json:api resource by checking if it implements . /// public static Type GetIdType(Type resourceType) { @@ -22,14 +21,14 @@ public static Type GetIdType(Type resourceType) } /// - /// Attempts to get a descriptor of the resource type. + /// Attempts to get a descriptor for the resource type. /// /// - /// True if the type is a valid json:api type (must implement ), false otherwise. + /// true if the type is a valid json:api type (must implement ); false, otherwise. /// internal static bool TryGetResourceDescriptor(Type type, out ResourceDescriptor descriptor) { - if (type.IsOrImplementsInterface(typeof(IIdentifiable))) + if (TypeHelper.IsOrImplementsInterface(type, typeof(IIdentifiable))) { descriptor = new ResourceDescriptor(type, GetIdType(type)); return true; @@ -38,11 +37,11 @@ internal static bool TryGetResourceDescriptor(Type type, out ResourceDescriptor return false; } /// - /// Get all implementations of the generic interface + /// Gets all implementations of the generic interface. /// - /// The assembly to search - /// The open generic type, e.g. `typeof(IResourceService<>)` - /// Parameters to the generic type + /// The assembly to search. + /// The open generic type, e.g. `typeof(IResourceService<>)`. + /// Parameters to the generic type. /// /// ), typeof(Article), typeof(Guid)); @@ -54,7 +53,7 @@ public static (Type implementation, Type registrationInterface) GetGenericInterf if (openGenericInterfaceType == null) throw new ArgumentNullException(nameof(openGenericInterfaceType)); if (genericInterfaceArguments == null) throw new ArgumentNullException(nameof(genericInterfaceArguments)); if (genericInterfaceArguments.Length == 0) throw new ArgumentException("No arguments supplied for the generic interface.", nameof(genericInterfaceArguments)); - if (openGenericInterfaceType.IsGenericType == false) throw new ArgumentException("Requested type is not a generic type.", nameof(openGenericInterfaceType)); + if (!openGenericInterfaceType.IsGenericType) throw new ArgumentException("Requested type is not a generic type.", nameof(openGenericInterfaceType)); foreach (var type in assembly.GetTypes()) { @@ -79,27 +78,27 @@ public static (Type implementation, Type registrationInterface) GetGenericInterf } /// - /// Get all derivatives of the concrete, generic type. + /// Gets all derivatives of the concrete, generic type. /// - /// The assembly to search - /// The open generic type, e.g. `typeof(ResourceDefinition<>)` - /// Parameters to the generic type + /// The assembly to search. + /// The open generic type, e.g. `typeof(ResourceDefinition<>)`. + /// Parameters to the generic type. /// /// ), typeof(Article)) /// ]]> /// - public static IEnumerable GetDerivedGenericTypes(Assembly assembly, Type openGenericType, params Type[] genericArguments) + public static IReadOnlyCollection GetDerivedGenericTypes(Assembly assembly, Type openGenericType, params Type[] genericArguments) { var genericType = openGenericType.MakeGenericType(genericArguments); - return GetDerivedTypes(assembly, genericType); + return GetDerivedTypes(assembly, genericType).ToArray(); } /// - /// Get all derivatives of the specified type. + /// Gets all derivatives of the specified type. /// - /// The assembly to search - /// The inherited type + /// The assembly to search. + /// The inherited type. /// /// /// GetDerivedGenericTypes(assembly, typeof(DbContext)) diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs new file mode 100644 index 0000000000..c1012d6100 --- /dev/null +++ b/src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.QueryStrings; + +namespace JsonApiDotNetCore.Controllers.Annotations +{ + /// + /// Used on an ASP.NET Core controller class to indicate which query string parameters are blocked. + /// + /// { } + /// ]]> + /// { } + /// ]]> + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] + public sealed class DisableQueryStringAttribute : Attribute + { + public IReadOnlyCollection ParameterNames { get; } + + public static readonly DisableQueryStringAttribute Empty = new DisableQueryStringAttribute(StandardQueryStringParameters.None); + + /// + /// Disables one or more of the builtin query parameters for a controller. + /// + public DisableQueryStringAttribute(StandardQueryStringParameters parameters) + { + var parameterNames = new List(); + + foreach (StandardQueryStringParameters value in Enum.GetValues(typeof(StandardQueryStringParameters))) + { + if (value != StandardQueryStringParameters.None && value != StandardQueryStringParameters.All && + parameters.HasFlag(value)) + { + parameterNames.Add(value.ToString().ToLowerInvariant()); + } + } + + ParameterNames = parameterNames; + } + + /// + /// It is allowed to use a comma-separated list of strings to indicate which query parameters + /// should be disabled, because the user may have defined custom query parameters that are + /// not included in the enum. + /// + public DisableQueryStringAttribute(string parameterNames) + { + if (parameterNames == null) throw new ArgumentNullException(nameof(parameterNames)); + + ParameterNames = parameterNames.Split(",").Select(x => x.Trim().ToLowerInvariant()).ToList(); + } + + public bool ContainsParameter(StandardQueryStringParameters parameter) + { + var name = parameter.ToString().ToLowerInvariant(); + return ParameterNames.Contains(name); + } + } +} diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/DisableRoutingConventionAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/DisableRoutingConventionAttribute.cs new file mode 100644 index 0000000000..ecd55377ab --- /dev/null +++ b/src/JsonApiDotNetCore/Controllers/Annotations/DisableRoutingConventionAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace JsonApiDotNetCore.Controllers.Annotations +{ + /// + /// Used on an ASP.NET Core controller class to indicate that a custom route is used instead of the built-in routing convention. + /// + /// { } + /// ]]> + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] + public sealed class DisableRoutingConventionAttribute : Attribute + { } +} diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/HttpReadOnlyAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/HttpReadOnlyAttribute.cs new file mode 100644 index 0000000000..46b0f93f79 --- /dev/null +++ b/src/JsonApiDotNetCore/Controllers/Annotations/HttpReadOnlyAttribute.cs @@ -0,0 +1,16 @@ +namespace JsonApiDotNetCore.Controllers.Annotations +{ + /// + /// Used on an ASP.NET Core controller class to indicate write actions must be blocked. + /// + /// + /// { + /// } + /// ]]> + public sealed class HttpReadOnlyAttribute : HttpRestrictAttribute + { + protected override string[] Methods { get; } = { "POST", "PATCH", "DELETE" }; + } +} diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/HttpRestrictAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/HttpRestrictAttribute.cs new file mode 100644 index 0000000000..dcb3fd88af --- /dev/null +++ b/src/JsonApiDotNetCore/Controllers/Annotations/HttpRestrictAttribute.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using JsonApiDotNetCore.Errors; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace JsonApiDotNetCore.Controllers.Annotations +{ + public abstract class HttpRestrictAttribute : ActionFilterAttribute + { + protected abstract string[] Methods { get; } + + public override async Task OnActionExecutionAsync( + ActionExecutingContext context, + ActionExecutionDelegate next) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (next == null) throw new ArgumentNullException(nameof(next)); + + var method = context.HttpContext.Request.Method; + + if (!CanExecuteAction(method)) + { + throw new RequestMethodNotAllowedException(new HttpMethod(method)); + } + + await next(); + } + + private bool CanExecuteAction(string requestMethod) + { + return !Methods.Contains(requestMethod); + } + } +} diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpDeleteAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpDeleteAttribute.cs new file mode 100644 index 0000000000..02a5fbdba1 --- /dev/null +++ b/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpDeleteAttribute.cs @@ -0,0 +1,16 @@ +namespace JsonApiDotNetCore.Controllers.Annotations +{ + /// + /// Used on an ASP.NET Core controller class to indicate the DELETE verb must be blocked. + /// + /// + /// { + /// } + /// ]]> + public sealed class NoHttpDeleteAttribute : HttpRestrictAttribute + { + protected override string[] Methods { get; } = { "DELETE" }; + } +} diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPatchAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPatchAttribute.cs new file mode 100644 index 0000000000..7039356db2 --- /dev/null +++ b/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPatchAttribute.cs @@ -0,0 +1,16 @@ +namespace JsonApiDotNetCore.Controllers.Annotations +{ + /// + /// Used on an ASP.NET Core controller class to indicate the PATCH verb must be blocked. + /// + /// + /// { + /// } + /// ]]> + public sealed class NoHttpPatchAttribute : HttpRestrictAttribute + { + protected override string[] Methods { get; } = { "PATCH" }; + } +} diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPostAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPostAttribute.cs new file mode 100644 index 0000000000..a40f1a3574 --- /dev/null +++ b/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPostAttribute.cs @@ -0,0 +1,16 @@ +namespace JsonApiDotNetCore.Controllers.Annotations +{ + /// + /// Used on an ASP.NET Core controller class to indicate the POST verb must be blocked. + /// + /// + /// { + /// } + /// ]]> + public sealed class NoHttpPostAttribute : HttpRestrictAttribute + { + protected override string[] Methods { get; } = { "POST" }; + } +} diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index b47e819d91..d33ad1aecc 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -1,58 +1,76 @@ +using System; using System.Net.Http; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Controllers { - public abstract class BaseJsonApiController : CoreJsonApiController where T : class, IIdentifiable + /// + /// Implements the foundational ASP.NET Core controller layer in the JsonApiDotNetCore architecture that delegates to a Resource Service. + /// + /// The resource type. + /// The resource identifier type. + public abstract class BaseJsonApiController : CoreJsonApiController where TResource : class, IIdentifiable { - private readonly IJsonApiOptions _jsonApiOptions; - private readonly IGetAllService _getAll; - private readonly IGetByIdService _getById; - private readonly IGetSecondaryService _getSecondary; - private readonly IGetRelationshipService _getRelationship; - private readonly ICreateService _create; - private readonly IUpdateService _update; - private readonly IUpdateRelationshipService _updateRelationships; - private readonly IDeleteService _delete; - private readonly ILogger> _logger; - + private readonly IJsonApiOptions _options; + private readonly IGetAllService _getAll; + private readonly IGetByIdService _getById; + private readonly IGetSecondaryService _getSecondary; + private readonly IGetRelationshipService _getRelationship; + private readonly ICreateService _create; + private readonly IUpdateService _update; + private readonly IUpdateRelationshipService _updateRelationships; + private readonly IDeleteService _delete; + private readonly TraceLogWriter> _traceWriter; + + /// + /// Creates an instance from a read/write service. + /// protected BaseJsonApiController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : this(jsonApiOptions, loggerFactory, resourceService, resourceService, resourceService, resourceService, + IResourceService resourceService) + : this(options, loggerFactory, resourceService, resourceService, resourceService, resourceService, resourceService, resourceService, resourceService, resourceService) { } + /// + /// Creates an instance from separate services for reading and writing. + /// protected BaseJsonApiController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceQueryService queryService = null, - IResourceCommandService commandService = null) - : this(jsonApiOptions, loggerFactory, queryService, queryService, queryService, queryService, commandService, + IResourceQueryService queryService = null, + IResourceCommandService commandService = null) + : this(options, loggerFactory, queryService, queryService, queryService, queryService, commandService, commandService, commandService, commandService) { } + /// + /// Creates an instance from separate services for the various individual read and write methods. + /// protected BaseJsonApiController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, - IGetAllService getAll = null, - IGetByIdService getById = null, - IGetSecondaryService getSecondary = null, - IGetRelationshipService getRelationship = null, - ICreateService create = null, - IUpdateService update = null, - IUpdateRelationshipService updateRelationships = null, - IDeleteService delete = null) + IGetAllService getAll = null, + IGetByIdService getById = null, + IGetSecondaryService getSecondary = null, + IGetRelationshipService getRelationship = null, + ICreateService create = null, + IUpdateService update = null, + IUpdateRelationshipService updateRelationships = null, + IDeleteService delete = null) { - _jsonApiOptions = jsonApiOptions; - _logger = loggerFactory.CreateLogger>(); + if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); + + _options = options ?? throw new ArgumentNullException(nameof(options)); + _traceWriter = new TraceLogWriter>(loggerFactory); _getAll = getAll; _getById = getById; _getSecondary = getSecondary; @@ -63,27 +81,40 @@ protected BaseJsonApiController( _delete = delete; } + /// + /// Gets a collection of top-level (non-nested) resources. + /// Example: GET /articles HTTP/1.1 + /// public virtual async Task GetAsync() { - _logger.LogTrace($"Entering {nameof(GetAsync)}()."); + _traceWriter.LogMethodStart(); if (_getAll == null) throw new RequestMethodNotAllowedException(HttpMethod.Get); var resources = await _getAll.GetAsync(); return Ok(resources); } + /// + /// Gets a single top-level (non-nested) resource by ID. + /// Example: /articles/1 + /// public virtual async Task GetAsync(TId id) { - _logger.LogTrace($"Entering {nameof(GetAsync)}('{id}')."); + _traceWriter.LogMethodStart(new {id}); if (_getById == null) throw new RequestMethodNotAllowedException(HttpMethod.Get); var resource = await _getById.GetAsync(id); return Ok(resource); } + /// + /// Gets a single resource relationship. + /// Example: GET /articles/1/relationships/author HTTP/1.1 + /// public virtual async Task GetRelationshipAsync(TId id, string relationshipName) { - _logger.LogTrace($"Entering {nameof(GetRelationshipAsync)}('{id}', '{relationshipName}')."); + _traceWriter.LogMethodStart(new {id, relationshipName}); + if (relationshipName == null) throw new ArgumentNullException(nameof(relationshipName)); if (_getRelationship == null) throw new RequestMethodNotAllowedException(HttpMethod.Get); var relationship = await _getRelationship.GetRelationshipAsync(id, relationshipName); @@ -91,18 +122,28 @@ public virtual async Task GetRelationshipAsync(TId id, string rel return Ok(relationship); } + /// + /// Gets a single resource or multiple resources at a nested endpoint. + /// Examples: + /// GET /articles/1/author HTTP/1.1 + /// GET /articles/1/revisions HTTP/1.1 + /// public virtual async Task GetSecondaryAsync(TId id, string relationshipName) { - _logger.LogTrace($"Entering {nameof(GetSecondaryAsync)}('{id}', '{relationshipName}')."); + _traceWriter.LogMethodStart(new {id, relationshipName}); + if (relationshipName == null) throw new ArgumentNullException(nameof(relationshipName)); if (_getSecondary == null) throw new RequestMethodNotAllowedException(HttpMethod.Get); var relationship = await _getSecondary.GetSecondaryAsync(id, relationshipName); return Ok(relationship); } - public virtual async Task PostAsync([FromBody] T resource) + /// + /// Creates a new resource. + /// + public virtual async Task PostAsync([FromBody] TResource resource) { - _logger.LogTrace($"Entering {nameof(PostAsync)}({(resource == null ? "null" : "object")})."); + _traceWriter.LogMethodStart(new {resource}); if (_create == null) throw new RequestMethodNotAllowedException(HttpMethod.Post); @@ -110,13 +151,13 @@ public virtual async Task PostAsync([FromBody] T resource) if (resource == null) throw new InvalidRequestBodyException(null, null, null); - if (!_jsonApiOptions.AllowClientGeneratedIds && !string.IsNullOrEmpty(resource.StringId)) + if (!_options.AllowClientGeneratedIds && !string.IsNullOrEmpty(resource.StringId)) throw new ResourceIdInPostRequestNotAllowedException(); - if (_jsonApiOptions.ValidateModelState && !ModelState.IsValid) + if (_options.ValidateModelState && !ModelState.IsValid) { - var namingStrategy = _jsonApiOptions.SerializerContractResolver.NamingStrategy; - throw new InvalidModelStateException(ModelState, typeof(T), _jsonApiOptions.IncludeExceptionStackTraceInErrors, namingStrategy); + var namingStrategy = _options.SerializerContractResolver.NamingStrategy; + throw new InvalidModelStateException(ModelState, typeof(TResource), _options.IncludeExceptionStackTraceInErrors, namingStrategy); } resource = await _create.CreateAsync(resource); @@ -124,36 +165,46 @@ public virtual async Task PostAsync([FromBody] T resource) return Created($"{HttpContext.Request.Path}/{resource.StringId}", resource); } - public virtual async Task PatchAsync(TId id, [FromBody] T resource) + /// + /// Updates an existing resource. May contain a partial set of attributes. + /// + public virtual async Task PatchAsync(TId id, [FromBody] TResource resource) { - _logger.LogTrace($"Entering {nameof(PatchAsync)}('{id}', {(resource == null ? "null" : "object")})."); + _traceWriter.LogMethodStart(new {id, resource}); if (_update == null) throw new RequestMethodNotAllowedException(HttpMethod.Patch); if (resource == null) throw new InvalidRequestBodyException(null, null, null); - if (_jsonApiOptions.ValidateModelState && !ModelState.IsValid) + if (_options.ValidateModelState && !ModelState.IsValid) { - var namingStrategy = _jsonApiOptions.SerializerContractResolver.NamingStrategy; - throw new InvalidModelStateException(ModelState, typeof(T), _jsonApiOptions.IncludeExceptionStackTraceInErrors, namingStrategy); + var namingStrategy = _options.SerializerContractResolver.NamingStrategy; + throw new InvalidModelStateException(ModelState, typeof(TResource), _options.IncludeExceptionStackTraceInErrors, namingStrategy); } var updated = await _update.UpdateAsync(id, resource); return updated == null ? Ok(null) : Ok(updated); } + /// + /// Updates a relationship. + /// public virtual async Task PatchRelationshipAsync(TId id, string relationshipName, [FromBody] object relationships) { - _logger.LogTrace($"Entering {nameof(PatchRelationshipAsync)}('{id}', '{relationshipName}', {(relationships == null ? "null" : "object")})."); + _traceWriter.LogMethodStart(new {id, relationshipName, relationships}); + if (relationshipName == null) throw new ArgumentNullException(nameof(relationshipName)); if (_updateRelationships == null) throw new RequestMethodNotAllowedException(HttpMethod.Patch); await _updateRelationships.UpdateRelationshipAsync(id, relationshipName, relationships); return Ok(); } + /// + /// Deletes a resource. + /// public virtual async Task DeleteAsync(TId id) { - _logger.LogTrace($"Entering {nameof(DeleteAsync)}('{id})."); + _traceWriter.LogMethodStart(new {id}); if (_delete == null) throw new RequestMethodNotAllowedException(HttpMethod.Delete); await _delete.DeleteAsync(id); @@ -162,35 +213,39 @@ public virtual async Task DeleteAsync(TId id) } } - public abstract class BaseJsonApiController : BaseJsonApiController where T : class, IIdentifiable + /// + public abstract class BaseJsonApiController : BaseJsonApiController where TResource : class, IIdentifiable { + /// protected BaseJsonApiController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService, resourceService) + IResourceService resourceService) + : base(options, loggerFactory, resourceService, resourceService) { } + /// protected BaseJsonApiController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceQueryService queryService = null, - IResourceCommandService commandService = null) - : base(jsonApiOptions, loggerFactory, queryService, commandService) + IResourceQueryService queryService = null, + IResourceCommandService commandService = null) + : base(options, loggerFactory, queryService, commandService) { } + /// protected BaseJsonApiController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, - IGetAllService getAll = null, - IGetByIdService getById = null, - IGetSecondaryService getSecondary = null, - IGetRelationshipService getRelationship = null, - ICreateService create = null, - IUpdateService update = null, - IUpdateRelationshipService updateRelationships = null, - IDeleteService delete = null) - : base(jsonApiOptions, loggerFactory, getAll, getById, getSecondary, getRelationship, create, update, + IGetAllService getAll = null, + IGetByIdService getById = null, + IGetSecondaryService getSecondary = null, + IGetRelationshipService getRelationship = null, + ICreateService create = null, + IUpdateService update = null, + IUpdateRelationshipService updateRelationships = null, + IDeleteService delete = null) + : base(options, loggerFactory, getAll, getById, getSecondary, getRelationship, create, update, updateRelationships, delete) { } } diff --git a/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs index aaf2cb6795..16685e7143 100644 --- a/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs @@ -1,20 +1,28 @@ +using System; using System.Collections.Generic; using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Mvc; namespace JsonApiDotNetCore.Controllers { + /// + /// Provides helper methods to raise json:api compliant errors from controller actions. + /// [ServiceFilter(typeof(IQueryStringActionFilter))] public abstract class CoreJsonApiController : ControllerBase { protected IActionResult Error(Error error) { + if (error == null) throw new ArgumentNullException(nameof(error)); + return Error(new[] {error}); } protected IActionResult Error(IEnumerable errors) { + if (errors == null) throw new ArgumentNullException(nameof(errors)); + var document = new ErrorDocument(errors); return new ObjectResult(document) diff --git a/src/JsonApiDotNetCore/Controllers/DisableQueryAttribute.cs b/src/JsonApiDotNetCore/Controllers/DisableQueryAttribute.cs deleted file mode 100644 index fa77d78a73..0000000000 --- a/src/JsonApiDotNetCore/Controllers/DisableQueryAttribute.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace JsonApiDotNetCore.Controllers -{ - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] - public sealed class DisableQueryAttribute : Attribute - { - private readonly List _parameterNames; - - public IReadOnlyCollection ParameterNames => _parameterNames.AsReadOnly(); - - public static readonly DisableQueryAttribute Empty = new DisableQueryAttribute(StandardQueryStringParameters.None); - - /// - /// Disables one or more of the builtin query parameters for a controller. - /// - public DisableQueryAttribute(StandardQueryStringParameters parameters) - { - _parameterNames = new List(); - - foreach (StandardQueryStringParameters value in Enum.GetValues(typeof(StandardQueryStringParameters))) - { - if (value != StandardQueryStringParameters.None && value != StandardQueryStringParameters.All && - parameters.HasFlag(value)) - { - _parameterNames.Add(value.ToString().ToLowerInvariant()); - } - } - } - - /// - /// It is allowed to use a comma-separated list of strings to indicate which query parameters - /// should be disabled, because the user may have defined custom query parameters that are - /// not included in the enum. - /// - public DisableQueryAttribute(string parameterNames) - { - _parameterNames = parameterNames.Split(",").Select(x => x.Trim().ToLowerInvariant()).ToList(); - } - - public bool ContainsParameter(StandardQueryStringParameters parameter) - { - var name = parameter.ToString().ToLowerInvariant(); - return _parameterNames.Contains(name); - } - } -} diff --git a/src/JsonApiDotNetCore/Controllers/DisableRoutingConventionAttribute.cs b/src/JsonApiDotNetCore/Controllers/DisableRoutingConventionAttribute.cs deleted file mode 100644 index 9b2090f6d6..0000000000 --- a/src/JsonApiDotNetCore/Controllers/DisableRoutingConventionAttribute.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace JsonApiDotNetCore.Controllers -{ - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] - public sealed class DisableRoutingConventionAttribute : Attribute - { } -} diff --git a/src/JsonApiDotNetCore/Controllers/HttpMethodRestrictionFilter.cs b/src/JsonApiDotNetCore/Controllers/HttpMethodRestrictionFilter.cs deleted file mode 100644 index 7f797419a4..0000000000 --- a/src/JsonApiDotNetCore/Controllers/HttpMethodRestrictionFilter.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using JsonApiDotNetCore.Exceptions; -using Microsoft.AspNetCore.Mvc.Filters; - -namespace JsonApiDotNetCore.Controllers -{ - public abstract class HttpRestrictAttribute : ActionFilterAttribute - { - protected abstract string[] Methods { get; } - - public override async Task OnActionExecutionAsync( - ActionExecutingContext context, - ActionExecutionDelegate next) - { - var method = context.HttpContext.Request.Method; - - if (CanExecuteAction(method) == false) - { - throw new RequestMethodNotAllowedException(new HttpMethod(method)); - } - - await next(); - } - - private bool CanExecuteAction(string requestMethod) - { - return Methods.Contains(requestMethod) == false; - } - } - - public sealed class HttpReadOnlyAttribute : HttpRestrictAttribute - { - protected override string[] Methods { get; } = new string[] { "POST", "PATCH", "DELETE" }; - } - - public sealed class NoHttpPostAttribute : HttpRestrictAttribute - { - protected override string[] Methods { get; } = new string[] { "POST" }; - } - - public sealed class NoHttpPatchAttribute : HttpRestrictAttribute - { - protected override string[] Methods { get; } = new string[] { "PATCH" }; - } - - public sealed class NoHttpDeleteAttribute : HttpRestrictAttribute - { - protected override string[] Methods { get; } = new string[] { "DELETE" }; - } -} diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiCommandController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiCommandController.cs index c02d019585..6e4b85dc4d 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiCommandController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiCommandController.cs @@ -1,45 +1,59 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Controllers { - public abstract class JsonApiCommandController : JsonApiCommandController where T : class, IIdentifiable + /// + /// The base class to derive resource-specific write-only controllers from. + /// This class delegates all work to but adds attributes for routing templates. + /// If you want to provide routing templates yourself, you should derive from BaseJsonApiController directly. + /// + /// The resource type. + /// The resource identifier type. + public abstract class JsonApiCommandController : BaseJsonApiController where TResource : class, IIdentifiable { + /// protected JsonApiCommandController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceCommandService commandService) - : base(jsonApiOptions, loggerFactory, commandService) - { } - } - - public abstract class JsonApiCommandController : BaseJsonApiController where T : class, IIdentifiable - { - protected JsonApiCommandController( - IJsonApiOptions jsonApiOptions, - ILoggerFactory loggerFactory, - IResourceCommandService commandService) - : base(jsonApiOptions, loggerFactory, null, commandService) + IResourceCommandService commandService) + : base(options, loggerFactory, null, commandService) { } + /// [HttpPost] - public override async Task PostAsync([FromBody] T resource) + public override async Task PostAsync([FromBody] TResource resource) => await base.PostAsync(resource); + /// [HttpPatch("{id}")] - public override async Task PatchAsync(TId id, [FromBody] T resource) + public override async Task PatchAsync(TId id, [FromBody] TResource resource) => await base.PatchAsync(id, resource); + /// [HttpPatch("{id}/relationships/{relationshipName}")] public override async Task PatchRelationshipAsync( TId id, string relationshipName, [FromBody] object relationships) => await base.PatchRelationshipAsync(id, relationshipName, relationships); + /// [HttpDelete("{id}")] public override async Task DeleteAsync(TId id) => await base.DeleteAsync(id); } + + /// + public abstract class JsonApiCommandController : JsonApiCommandController where TResource : class, IIdentifiable + { + /// + protected JsonApiCommandController( + IJsonApiOptions options, + ILoggerFactory loggerFactory, + IResourceCommandService commandService) + : base(options, loggerFactory, commandService) + { } + } } diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs index 25725ec5ca..1fd42b97aa 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs @@ -1,90 +1,110 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Controllers { - public class JsonApiController : BaseJsonApiController where T : class, IIdentifiable + /// + /// The base class to derive resource-specific controllers from. + /// This class delegates all work to but adds attributes for routing templates. + /// If you want to provide routing templates yourself, you should derive from BaseJsonApiController directly. + /// + /// The resource type. + /// The resource identifier type. + public class JsonApiController : BaseJsonApiController where TResource : class, IIdentifiable { + /// public JsonApiController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + IResourceService resourceService) + : base(options, loggerFactory, resourceService) { } + /// public JsonApiController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, - IGetAllService getAll = null, - IGetByIdService getById = null, - IGetSecondaryService getSecondary = null, - IGetRelationshipService getRelationship = null, - ICreateService create = null, - IUpdateService update = null, - IUpdateRelationshipService updateRelationships = null, - IDeleteService delete = null) - : base(jsonApiOptions, loggerFactory, getAll, getById, getSecondary, getRelationship, create, update, + IGetAllService getAll = null, + IGetByIdService getById = null, + IGetSecondaryService getSecondary = null, + IGetRelationshipService getRelationship = null, + ICreateService create = null, + IUpdateService update = null, + IUpdateRelationshipService updateRelationships = null, + IDeleteService delete = null) + : base(options, loggerFactory, getAll, getById, getSecondary, getRelationship, create, update, updateRelationships, delete) { } + /// [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 GetRelationshipAsync(TId id, string relationshipName) => await base.GetRelationshipAsync(id, relationshipName); + /// [HttpGet("{id}/{relationshipName}")] public override async Task GetSecondaryAsync(TId id, string relationshipName) => await base.GetSecondaryAsync(id, relationshipName); + /// [HttpPost] - public override async Task PostAsync([FromBody] T resource) + public override async Task PostAsync([FromBody] TResource resource) => await base.PostAsync(resource); + /// [HttpPatch("{id}")] - public override async Task PatchAsync(TId id, [FromBody] T resource) + public override async Task PatchAsync(TId id, [FromBody] TResource resource) { return await base.PatchAsync(id, resource); } + /// [HttpPatch("{id}/relationships/{relationshipName}")] public override async Task PatchRelationshipAsync( TId id, string relationshipName, [FromBody] object relationships) => await base.PatchRelationshipAsync(id, relationshipName, relationships); + /// [HttpDelete("{id}")] public override async Task DeleteAsync(TId id) => await base.DeleteAsync(id); } - public class JsonApiController : JsonApiController where T : class, IIdentifiable + /// + public class JsonApiController : JsonApiController where TResource : class, IIdentifiable { + /// public JsonApiController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + IResourceService resourceService) + : base(options, loggerFactory, resourceService) { } + /// public JsonApiController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, - IGetAllService getAll = null, - IGetByIdService getById = null, - IGetSecondaryService getSecondary = null, - IGetRelationshipService getRelationship = null, - ICreateService create = null, - IUpdateService update = null, - IUpdateRelationshipService updateRelationships = null, - IDeleteService delete = null) - : base(jsonApiOptions, loggerFactory, getAll, getById, getSecondary, getRelationship, create, update, + IGetAllService getAll = null, + IGetByIdService getById = null, + IGetSecondaryService getSecondary = null, + IGetRelationshipService getRelationship = null, + ICreateService create = null, + IUpdateService update = null, + IUpdateRelationshipService updateRelationships = null, + IDeleteService delete = null) + : base(options, loggerFactory, getAll, getById, getSecondary, getRelationship, create, update, updateRelationships, delete) { } } diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs index 9e4958cf3b..89af9d95c8 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs @@ -1,43 +1,57 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Controllers { - public abstract class JsonApiQueryController : JsonApiQueryController where T : class, IIdentifiable + /// + /// The base class to derive resource-specific read-only controllers from. + /// This class delegates all work to but adds attributes for routing templates. + /// If you want to provide routing templates yourself, you should derive from BaseJsonApiController directly. + /// + /// The resource type. + /// The resource identifier type. + public abstract class JsonApiQueryController : BaseJsonApiController where TResource : class, IIdentifiable { + /// protected JsonApiQueryController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions context, ILoggerFactory loggerFactory, - IResourceQueryService queryService) - : base(jsonApiOptions, loggerFactory, queryService) - { } - } - - public abstract class JsonApiQueryController : BaseJsonApiController where T : class, IIdentifiable - { - protected JsonApiQueryController( - IJsonApiOptions jsonApiContext, - ILoggerFactory loggerFactory, - IResourceQueryService queryService) - : base(jsonApiContext, loggerFactory, queryService) + IResourceQueryService queryService) + : base(context, loggerFactory, queryService) { } + /// [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 GetRelationshipAsync(TId id, string relationshipName) => await base.GetRelationshipAsync(id, relationshipName); + /// [HttpGet("{id}/{relationshipName}")] public override async Task GetSecondaryAsync(TId id, string relationshipName) => await base.GetSecondaryAsync(id, relationshipName); } + + /// + public abstract class JsonApiQueryController : JsonApiQueryController where TResource : class, IIdentifiable + { + /// + protected JsonApiQueryController( + IJsonApiOptions options, + ILoggerFactory loggerFactory, + IResourceQueryService queryService) + : base(options, loggerFactory, queryService) + { } + } } diff --git a/src/JsonApiDotNetCore/Data/IDbContextResolver.cs b/src/JsonApiDotNetCore/Data/IDbContextResolver.cs deleted file mode 100644 index f1b4533f26..0000000000 --- a/src/JsonApiDotNetCore/Data/IDbContextResolver.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace JsonApiDotNetCore.Data -{ - public interface IDbContextResolver - { - DbContext GetContext(); - } -} diff --git a/src/JsonApiDotNetCore/Data/IResourceRepository.cs b/src/JsonApiDotNetCore/Data/IResourceRepository.cs deleted file mode 100644 index 100ea63961..0000000000 --- a/src/JsonApiDotNetCore/Data/IResourceRepository.cs +++ /dev/null @@ -1,15 +0,0 @@ -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCore.Data -{ - public interface IResourceRepository - : IResourceRepository - where TResource : class, IIdentifiable - { } - - public interface IResourceRepository - : IResourceReadRepository, - IResourceWriteRepository - where TResource : class, IIdentifiable - { } -} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Data/IResourceWriteRepository.cs b/src/JsonApiDotNetCore/Data/IResourceWriteRepository.cs deleted file mode 100644 index 96549537b6..0000000000 --- a/src/JsonApiDotNetCore/Data/IResourceWriteRepository.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; - -namespace JsonApiDotNetCore.Data -{ - public interface IResourceWriteRepository - : IResourceWriteRepository - where TResource : class, IIdentifiable - { } - - public interface IResourceWriteRepository - where TResource : class, IIdentifiable - { - Task CreateAsync(TResource resource); - - Task UpdateAsync(TResource requestResource, TResource databaseResource); - - Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds); - - Task DeleteAsync(TId id); - - void FlushFromCache(TResource resource); - } -} diff --git a/src/JsonApiDotNetCore/Errors/InvalidConfigurationException.cs b/src/JsonApiDotNetCore/Errors/InvalidConfigurationException.cs new file mode 100644 index 0000000000..c838e2ca9d --- /dev/null +++ b/src/JsonApiDotNetCore/Errors/InvalidConfigurationException.cs @@ -0,0 +1,13 @@ +using System; + +namespace JsonApiDotNetCore.Errors +{ + /// + /// The error that is thrown when configured usage of this library is invalid. + /// + public sealed class InvalidConfigurationException : Exception + { + public InvalidConfigurationException(string message, Exception innerException = null) + : base(message, innerException) { } + } +} diff --git a/src/JsonApiDotNetCore/Exceptions/InvalidModelStateException.cs b/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs similarity index 75% rename from src/JsonApiDotNetCore/Exceptions/InvalidModelStateException.cs rename to src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs index ddd1e790c2..038dee1166 100644 --- a/src/JsonApiDotNetCore/Exceptions/InvalidModelStateException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs @@ -3,40 +3,43 @@ using System.Linq; using System.Net; using System.Reflection; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Mvc.ModelBinding; using Newtonsoft.Json.Serialization; -namespace JsonApiDotNetCore.Exceptions +namespace JsonApiDotNetCore.Errors { /// /// The error that is thrown when model state validation fails. /// public class InvalidModelStateException : Exception { - public IList Errors { get; } + public IReadOnlyCollection Errors { get; } public InvalidModelStateException(ModelStateDictionary modelState, Type resourceType, bool includeExceptionStackTraceInErrors, NamingStrategy namingStrategy) { + if (modelState == null) throw new ArgumentNullException(nameof(modelState)); + if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + if (namingStrategy == null) throw new ArgumentNullException(nameof(namingStrategy)); + Errors = FromModelState(modelState, resourceType, includeExceptionStackTraceInErrors, namingStrategy); } - private static List FromModelState(ModelStateDictionary modelState, Type resourceType, + private static IReadOnlyCollection FromModelState(ModelStateDictionary modelState, Type resourceType, bool includeExceptionStackTraceInErrors, NamingStrategy namingStrategy) { List errors = new List(); - foreach (var pair in modelState.Where(x => x.Value.Errors.Any())) + foreach (var (propertyName, entry) in modelState.Where(x => x.Value.Errors.Any())) { - var propertyName = pair.Key; PropertyInfo property = resourceType.GetProperty(propertyName); string attributeName = property.GetCustomAttribute().PublicName ?? namingStrategy.GetPropertyName(property.Name, false); - foreach (var modelError in pair.Value.Errors) + foreach (var modelError in entry.Errors) { if (modelError.Exception is JsonApiException jsonApiException) { diff --git a/src/JsonApiDotNetCore/Exceptions/InvalidQueryException.cs b/src/JsonApiDotNetCore/Errors/InvalidQueryException.cs similarity index 79% rename from src/JsonApiDotNetCore/Exceptions/InvalidQueryException.cs rename to src/JsonApiDotNetCore/Errors/InvalidQueryException.cs index c803bd68a6..3b68dfc931 100644 --- a/src/JsonApiDotNetCore/Exceptions/InvalidQueryException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidQueryException.cs @@ -1,9 +1,9 @@ using System; using System.Net; -using JsonApiDotNetCore.Models.JsonApiDocuments; using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Exceptions +namespace JsonApiDotNetCore.Errors { /// /// The error that is thrown when translating a to Entity Framework Core fails. @@ -14,7 +14,7 @@ public InvalidQueryException(string reason, Exception exception) : base(new Error(HttpStatusCode.BadRequest) { Title = reason, - Detail = exception.Message + Detail = exception?.Message }, exception) { } diff --git a/src/JsonApiDotNetCore/Exceptions/InvalidQueryStringParameterException.cs b/src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs similarity index 67% rename from src/JsonApiDotNetCore/Exceptions/InvalidQueryStringParameterException.cs rename to src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs index 104db23a4a..12e66112d0 100644 --- a/src/JsonApiDotNetCore/Exceptions/InvalidQueryStringParameterException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs @@ -1,8 +1,8 @@ using System; using System.Net; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Exceptions +namespace JsonApiDotNetCore.Errors { /// /// The error that is thrown when processing the request fails due to an error in the request query string. @@ -12,13 +12,7 @@ public sealed class InvalidQueryStringParameterException : JsonApiException public string QueryParameterName { get; } public InvalidQueryStringParameterException(string queryParameterName, string genericMessage, - string specificMessage) - : this(queryParameterName, genericMessage, specificMessage, null) - { - } - - public InvalidQueryStringParameterException(string queryParameterName, string genericMessage, - string specificMessage, Exception innerException) + string specificMessage, Exception innerException = null) : base(new Error(HttpStatusCode.BadRequest) { Title = genericMessage, diff --git a/src/JsonApiDotNetCore/Exceptions/InvalidRequestBodyException.cs b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs similarity index 94% rename from src/JsonApiDotNetCore/Exceptions/InvalidRequestBodyException.cs rename to src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs index 5f976001e0..1c91f34e9e 100644 --- a/src/JsonApiDotNetCore/Exceptions/InvalidRequestBodyException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs @@ -1,8 +1,8 @@ using System; using System.Net; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Exceptions +namespace JsonApiDotNetCore.Errors { /// /// The error that is thrown when deserializing the request body fails. diff --git a/src/JsonApiDotNetCore/Exceptions/JsonApiException.cs b/src/JsonApiDotNetCore/Errors/JsonApiException.cs similarity index 71% rename from src/JsonApiDotNetCore/Exceptions/JsonApiException.cs rename to src/JsonApiDotNetCore/Errors/JsonApiException.cs index 381040b495..81609a740f 100644 --- a/src/JsonApiDotNetCore/Exceptions/JsonApiException.cs +++ b/src/JsonApiDotNetCore/Errors/JsonApiException.cs @@ -1,9 +1,12 @@ using System; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using Newtonsoft.Json; -namespace JsonApiDotNetCore.Exceptions +namespace JsonApiDotNetCore.Errors { + /// + /// The base class for an that represents a json:api error object in an unsuccessful response. + /// public class JsonApiException : Exception { private static readonly JsonSerializerSettings _errorSerializerSettings = new JsonSerializerSettings @@ -14,12 +17,7 @@ public class JsonApiException : Exception public Error Error { get; } - public JsonApiException(Error error) - : this(error, null) - { - } - - public JsonApiException(Error error, Exception innerException) + public JsonApiException(Error error, Exception innerException = null) : base(error.Title, innerException) { Error = error; diff --git a/src/JsonApiDotNetCore/Exceptions/RelationshipNotFoundException.cs b/src/JsonApiDotNetCore/Errors/RelationshipNotFoundException.cs similarity index 86% rename from src/JsonApiDotNetCore/Exceptions/RelationshipNotFoundException.cs rename to src/JsonApiDotNetCore/Errors/RelationshipNotFoundException.cs index a8c2fc3be2..a56091151a 100644 --- a/src/JsonApiDotNetCore/Exceptions/RelationshipNotFoundException.cs +++ b/src/JsonApiDotNetCore/Errors/RelationshipNotFoundException.cs @@ -1,7 +1,7 @@ using System.Net; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Exceptions +namespace JsonApiDotNetCore.Errors { /// /// The error that is thrown when a relationship does not exist. diff --git a/src/JsonApiDotNetCore/Exceptions/RequestMethodNotAllowedException.cs b/src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs similarity index 88% rename from src/JsonApiDotNetCore/Exceptions/RequestMethodNotAllowedException.cs rename to src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs index 3dfbffa2ef..7444d6cc46 100644 --- a/src/JsonApiDotNetCore/Exceptions/RequestMethodNotAllowedException.cs +++ b/src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs @@ -1,8 +1,8 @@ using System.Net; using System.Net.Http; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Exceptions +namespace JsonApiDotNetCore.Errors { /// /// The error that is thrown when a request is received that contains an unsupported HTTP verb. diff --git a/src/JsonApiDotNetCore/Exceptions/ResourceIdInPostRequestNotAllowedException.cs b/src/JsonApiDotNetCore/Errors/ResourceIdInPostRequestNotAllowedException.cs similarity index 78% rename from src/JsonApiDotNetCore/Exceptions/ResourceIdInPostRequestNotAllowedException.cs rename to src/JsonApiDotNetCore/Errors/ResourceIdInPostRequestNotAllowedException.cs index fdf6a0287d..6b621faa6f 100644 --- a/src/JsonApiDotNetCore/Exceptions/ResourceIdInPostRequestNotAllowedException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceIdInPostRequestNotAllowedException.cs @@ -1,7 +1,7 @@ using System.Net; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Exceptions +namespace JsonApiDotNetCore.Errors { /// /// The error that is thrown when a POST request is received that contains a client-generated ID. @@ -11,7 +11,7 @@ public sealed class ResourceIdInPostRequestNotAllowedException : JsonApiExceptio public ResourceIdInPostRequestNotAllowedException() : base(new Error(HttpStatusCode.Forbidden) { - Title = "Specifying the resource id in POST requests is not allowed.", + Title = "Specifying the resource ID in POST requests is not allowed.", Source = { Pointer = "/data/id" diff --git a/src/JsonApiDotNetCore/Exceptions/ResourceIdMismatchException.cs b/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs similarity index 56% rename from src/JsonApiDotNetCore/Exceptions/ResourceIdMismatchException.cs rename to src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs index 72c91a0191..b463bbee0d 100644 --- a/src/JsonApiDotNetCore/Exceptions/ResourceIdMismatchException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs @@ -1,18 +1,18 @@ using System.Net; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Exceptions +namespace JsonApiDotNetCore.Errors { /// - /// The error that is thrown when the resource id in the request body does not match the id in the current endpoint URL. + /// The error that is thrown when the resource ID in the request body does not match the ID in the current endpoint URL. /// public sealed class ResourceIdMismatchException : JsonApiException { public ResourceIdMismatchException(string bodyId, string endpointId, string requestPath) : base(new Error(HttpStatusCode.Conflict) { - Title = "Resource id mismatch between request body and endpoint URL.", - Detail = $"Expected resource id '{endpointId}' in PATCH request body at endpoint '{requestPath}', instead of '{bodyId}'." + Title = "Resource ID mismatch between request body and endpoint URL.", + Detail = $"Expected resource ID '{endpointId}' in PATCH request body at endpoint '{requestPath}', instead of '{bodyId}'." }) { } diff --git a/src/JsonApiDotNetCore/Exceptions/ResourceNotFoundException.cs b/src/JsonApiDotNetCore/Errors/ResourceNotFoundException.cs similarity index 75% rename from src/JsonApiDotNetCore/Exceptions/ResourceNotFoundException.cs rename to src/JsonApiDotNetCore/Errors/ResourceNotFoundException.cs index 28f8ac73cf..22f6a57eaa 100644 --- a/src/JsonApiDotNetCore/Exceptions/ResourceNotFoundException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceNotFoundException.cs @@ -1,7 +1,7 @@ using System.Net; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Exceptions +namespace JsonApiDotNetCore.Errors { /// /// The error that is thrown when a resource does not exist. @@ -11,7 +11,7 @@ public sealed class ResourceNotFoundException : JsonApiException public ResourceNotFoundException(string resourceId, string resourceType) : base(new Error(HttpStatusCode.NotFound) { Title = "The requested resource does not exist.", - Detail = $"Resource of type '{resourceType}' with id '{resourceId}' does not exist." + Detail = $"Resource of type '{resourceType}' with ID '{resourceId}' does not exist." }) { } diff --git a/src/JsonApiDotNetCore/Exceptions/ResourceTypeMismatchException.cs b/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs similarity index 82% rename from src/JsonApiDotNetCore/Exceptions/ResourceTypeMismatchException.cs rename to src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs index 05eca67e1d..42159c9214 100644 --- a/src/JsonApiDotNetCore/Exceptions/ResourceTypeMismatchException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs @@ -1,9 +1,9 @@ using System.Net; using System.Net.Http; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Exceptions +namespace JsonApiDotNetCore.Errors { /// /// The error that is thrown when the resource type in the request body does not match the type expected at the current endpoint URL. @@ -14,7 +14,7 @@ public ResourceTypeMismatchException(HttpMethod method, string requestPath, Reso : base(new Error(HttpStatusCode.Conflict) { Title = "Resource type mismatch between request body and endpoint URL.", - Detail = $"Expected resource of type '{expected.ResourceName}' in {method} request body at endpoint '{requestPath}', instead of '{actual.ResourceName}'." + Detail = $"Expected resource of type '{expected.ResourceName}' in {method} request body at endpoint '{requestPath}', instead of '{actual?.ResourceName}'." }) { } diff --git a/src/JsonApiDotNetCore/Exceptions/UnsuccessfulActionResultException.cs b/src/JsonApiDotNetCore/Errors/UnsuccessfulActionResultException.cs similarity index 87% rename from src/JsonApiDotNetCore/Exceptions/UnsuccessfulActionResultException.cs rename to src/JsonApiDotNetCore/Errors/UnsuccessfulActionResultException.cs index 7bf8bb89fc..7f8cd6fc9d 100644 --- a/src/JsonApiDotNetCore/Exceptions/UnsuccessfulActionResultException.cs +++ b/src/JsonApiDotNetCore/Errors/UnsuccessfulActionResultException.cs @@ -1,8 +1,9 @@ +using System; using System.Net; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Mvc; -namespace JsonApiDotNetCore.Exceptions +namespace JsonApiDotNetCore.Errors { /// /// The error that is thrown when an with non-success status is returned from a controller method. @@ -24,6 +25,8 @@ public UnsuccessfulActionResultException(ProblemDetails problemDetails) private static Error ToError(ProblemDetails problemDetails) { + if (problemDetails == null) throw new ArgumentNullException(nameof(problemDetails)); + var status = problemDetails.Status != null ? (HttpStatusCode) problemDetails.Status.Value : HttpStatusCode.InternalServerError; diff --git a/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs b/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs deleted file mode 100644 index 7e55386b88..0000000000 --- a/src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using JsonApiDotNetCore.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage; - -namespace JsonApiDotNetCore.Extensions -{ - public static class DbContextExtensions - { - /// - /// Determines whether or not EF is already tracking an entity of the same Type and Id - /// and returns that entity. - /// - internal static TEntity GetTrackedEntity(this DbContext context, TEntity entity) - where TEntity : IIdentifiable - { - if (entity == null) - throw new ArgumentNullException(nameof(entity)); - - var entityEntry = context.ChangeTracker - .Entries() - .FirstOrDefault(entry => - entry.Entity.GetType() == entity.GetType() && - ((IIdentifiable) entry.Entity).StringId == entity.StringId); - - return (TEntity) entityEntry?.Entity; - } - - /// - /// Gets the current transaction or creates a new one. - /// If a transaction already exists, commit, rollback and dispose - /// will not be called. It is assumed the creator of the original - /// transaction should be responsible for disposal. - /// - /// - /// - /// - /// using(var transaction = _context.GetCurrentOrCreateTransaction()) - /// { - /// // perform multiple operations on the context and then save... - /// _context.SaveChanges(); - /// } - /// - /// - public static async Task GetCurrentOrCreateTransactionAsync(this DbContext context) - => await SafeTransactionProxy.GetOrCreateAsync(context.Database); - } - - /// - /// Gets the current transaction or creates a new one. - /// If a transaction already exists, commit, rollback and dispose - /// will not be called. It is assumed the creator of the original - /// transaction should be responsible for disposal. - /// - internal struct SafeTransactionProxy : IDbContextTransaction - { - private readonly bool _shouldExecute; - private readonly IDbContextTransaction _transaction; - - private SafeTransactionProxy(IDbContextTransaction transaction, bool shouldExecute) - { - _transaction = transaction; - _shouldExecute = shouldExecute; - } - - public static async Task GetOrCreateAsync(DatabaseFacade databaseFacade) - => (databaseFacade.CurrentTransaction != null) - ? new SafeTransactionProxy(databaseFacade.CurrentTransaction, shouldExecute: false) - : new SafeTransactionProxy(await databaseFacade.BeginTransactionAsync(), shouldExecute: true); - - /// - public Guid TransactionId => _transaction.TransactionId; - - /// - public void Commit() => Proxy(t => t.Commit()); - - /// - public void Rollback() => Proxy(t => t.Rollback()); - - /// - public void Dispose() => Proxy(t => t.Dispose()); - - private void Proxy(Action func) - { - if(_shouldExecute) - func(_transaction); - } - - public Task CommitAsync(CancellationToken cancellationToken = default) - { - return _transaction.CommitAsync(cancellationToken); - } - - public Task RollbackAsync(CancellationToken cancellationToken = default) - { - return _transaction.RollbackAsync(cancellationToken); - } - - public ValueTask DisposeAsync() - { - return _transaction.DisposeAsync(); - } - } -} diff --git a/src/JsonApiDotNetCore/Extensions/SystemCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/SystemCollectionExtensions.cs deleted file mode 100644 index 04eb2bd8e4..0000000000 --- a/src/JsonApiDotNetCore/Extensions/SystemCollectionExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace JsonApiDotNetCore.Extensions -{ - internal static class SystemCollectionExtensions - { - public static void AddRange(this ICollection source, IEnumerable items) - { - if (source == null) throw new ArgumentNullException(nameof(source)); - if (items == null) throw new ArgumentNullException(nameof(items)); - - foreach (var item in items) - { - source.Add(item); - } - } - - public static void AddRange(this IList source, IEnumerable items) - { - if (source == null) throw new ArgumentNullException(nameof(source)); - if (items == null) throw new ArgumentNullException(nameof(items)); - - foreach (var item in items) - { - source.Add(item); - } - } - } -} diff --git a/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs b/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs deleted file mode 100644 index e8ea2ebdf9..0000000000 --- a/src/JsonApiDotNetCore/Extensions/TypeExtensions.cs +++ /dev/null @@ -1,100 +0,0 @@ -using JsonApiDotNetCore.Internal; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace JsonApiDotNetCore.Extensions -{ - internal static class TypeExtensions - { - /// - /// Extension to use the LINQ cast method in a non-generic way: - /// - /// Type targetType = typeof(TResource) - /// ((IList)myList).CopyToList(targetType). - /// - /// - public static IList CopyToList(this IEnumerable copyFrom, Type elementType, Converter elementConverter = null) - { - Type collectionType = typeof(List<>).MakeGenericType(elementType); - - if (elementConverter != null) - { - var converted = copyFrom.Cast().Select(element => elementConverter(element)); - return (IList) CopyToTypedCollection(converted, collectionType); - } - - return (IList)CopyToTypedCollection(copyFrom, collectionType); - } - - /// - /// Creates a collection instance based on the specified collection type and copies the specified elements into it. - /// - /// Source to copy from. - /// Target collection type, for example: typeof(List{Article}) or typeof(ISet{Person}). - /// - public static IEnumerable CopyToTypedCollection(this IEnumerable source, Type collectionType) - { - if (source == null) throw new ArgumentNullException(nameof(source)); - if (collectionType == null) throw new ArgumentNullException(nameof(collectionType)); - - var concreteCollectionType = collectionType.ToConcreteCollectionType(); - dynamic concreteCollectionInstance = TypeHelper.CreateInstance(concreteCollectionType); - - foreach (var item in source) - { - concreteCollectionInstance.Add((dynamic) item); - } - - return concreteCollectionInstance; - } - - /// - /// Whether the specified source type implements or equals the specified interface. - /// - public static bool IsOrImplementsInterface(this Type source, Type interfaceType) - { - if (interfaceType == null) - { - throw new ArgumentNullException(nameof(interfaceType)); - } - - if (source == null) - { - return false; - } - - return source == interfaceType || source.GetInterfaces().Any(type => type == interfaceType); - } - - public static bool HasSingleConstructorWithoutParameters(this Type type) - { - ConstructorInfo[] constructors = type.GetConstructors().Where(c => !c.IsStatic).ToArray(); - - return constructors.Length == 1 && constructors[0].GetParameters().Length == 0; - } - - public static ConstructorInfo GetLongestConstructor(this Type type) - { - ConstructorInfo[] constructors = type.GetConstructors().Where(c => !c.IsStatic).ToArray(); - - ConstructorInfo bestMatch = constructors[0]; - int maxParameterLength = constructors[0].GetParameters().Length; - - for (int index = 1; index < constructors.Length; index++) - { - var constructor = constructors[index]; - int length = constructor.GetParameters().Length; - if (length > maxParameterLength) - { - bestMatch = constructor; - maxParameterLength = length; - } - } - - return bestMatch; - } - } -} diff --git a/src/JsonApiDotNetCore/Graph/IServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/IServiceDiscoveryFacade.cs deleted file mode 100644 index 53e4e80cc5..0000000000 --- a/src/JsonApiDotNetCore/Graph/IServiceDiscoveryFacade.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Reflection; - -namespace JsonApiDotNetCore.Graph -{ - public interface IServiceDiscoveryFacade - { - ServiceDiscoveryFacade AddAssembly(Assembly assembly); - ServiceDiscoveryFacade AddCurrentAssembly(); - } -} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Graph/ResourceIdMapper.cs b/src/JsonApiDotNetCore/Graph/ResourceIdMapper.cs deleted file mode 100644 index bf70a7a163..0000000000 --- a/src/JsonApiDotNetCore/Graph/ResourceIdMapper.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace JsonApiDotNetCore.Graph -{ - /// - /// Provides an interface for formatting relationship identifiers from the navigation property name - /// - public interface IRelatedIdMapper - { - /// - /// Get the internal property name for the database mapped identifier property - /// - /// - /// - /// - /// RelatedIdMapper.GetRelatedIdPropertyName("Article"); - /// // "ArticleId" - /// - /// - string GetRelatedIdPropertyName(string propertyName); - } - - /// - public sealed class RelatedIdMapper : IRelatedIdMapper - { - /// - public string GetRelatedIdPropertyName(string propertyName) => propertyName + "Id"; - } -} diff --git a/src/JsonApiDotNetCore/Graph/ResourceNameFormatter.cs b/src/JsonApiDotNetCore/Graph/ResourceNameFormatter.cs deleted file mode 100644 index 1c4902db0b..0000000000 --- a/src/JsonApiDotNetCore/Graph/ResourceNameFormatter.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Reflection; -using Humanizer; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models.Annotation; -using Newtonsoft.Json.Serialization; - -namespace JsonApiDotNetCore.Graph -{ - internal sealed class ResourceNameFormatter - { - private readonly NamingStrategy _namingStrategy; - - public ResourceNameFormatter(IJsonApiOptions options) - { - _namingStrategy = options.SerializerContractResolver.NamingStrategy; - } - - /// - /// Gets the publicly visible resource name from the internal type name. - /// - public string FormatResourceName(Type type) - { - try - { - // check the class definition first - // [Resource("models"] public class Model : Identifiable { /* ... */ } - if (type.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute) - { - return attribute.ResourceName; - } - - return _namingStrategy.GetPropertyName(type.Name.Pluralize(), false); - } - catch (InvalidOperationException exception) - { - throw new InvalidOperationException($"Cannot define multiple {nameof(ResourceAttribute)}s on type '{type}'.", exception); - } - } - } -} diff --git a/src/JsonApiDotNetCore/Hooks/IResourceHookContainer.cs b/src/JsonApiDotNetCore/Hooks/IResourceHookContainer.cs deleted file mode 100644 index 8f7cc1636f..0000000000 --- a/src/JsonApiDotNetCore/Hooks/IResourceHookContainer.cs +++ /dev/null @@ -1,238 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Hooks -{ - /// - /// Not meant for public usage. Used internally in the - /// - public interface IResourceHookContainer { } - - /// - /// Implement this interface to implement business logic hooks on . - /// - public interface IResourceHookContainer - : IReadHookContainer, IDeleteHookContainer, ICreateHookContainer, - IUpdateHookContainer, IOnReturnHookContainer, IResourceHookContainer - where TResource : class, IIdentifiable { } - - /// - /// Read hooks container - /// - public interface IReadHookContainer where TResource : class, IIdentifiable - { - /// - /// Implement this hook to run custom logic in the - /// layer just before reading resources of type . - /// - /// An enum indicating from where the hook was triggered. - /// Indicates whether the to be queried resources are the primary request resources or if they were included - /// The string id of the requested resource, in the case of - void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null); - /// - /// Implement this hook to run custom logic in the - /// layer just after reading resources of type . - /// - /// The unique set of affected resources. - /// An enum indicating from where the hook was triggered. - /// A boolean to indicate whether the resources in this hook execution are the primary resources of the request, - /// or if they were included as a relationship - void AfterRead(HashSet resources, ResourcePipeline pipeline, bool isIncluded = false); - } - - /// - /// Create hooks container - /// - public interface ICreateHookContainer where TResource : class, IIdentifiable - { - /// - /// Implement this hook to run custom logic in the - /// layer just before creation of resources of type . - /// - /// For the pipeline, - /// will typically contain one entry. - /// - /// The returned may be a subset - /// of , in which case the operation of the - /// pipeline will not be executed for the omitted resources. The returned - /// set may also contain custom changes of the properties on the resources. - /// - /// If new relationships are to be created with the to-be-created resources, - /// this will be reflected by the corresponding NavigationProperty being set. - /// For each of these relationships, the - /// hook is fired after the execution of this hook. - /// - /// The transformed resource set - /// The unique set of affected resources. - /// An enum indicating from where the hook was triggered. - IEnumerable BeforeCreate(IResourceHashSet resources, ResourcePipeline pipeline); - - /// - /// Implement this hook to run custom logic in the - /// layer just after creation of resources of type . - /// - /// If relationships were created with the created resources, this will - /// be reflected by the corresponding NavigationProperty being set. - /// For each of these relationships, the - /// hook is fired after the execution of this hook. - /// - /// The transformed resource set - /// The unique set of affected resources. - /// An enum indicating from where the hook was triggered. - void AfterCreate(HashSet resources, ResourcePipeline pipeline); - } - - /// - /// update hooks container - /// - public interface IUpdateHookContainer where TResource : class, IIdentifiable - { - /// - /// Implement this hook to run custom logic in the - /// layer just before updating resources of type . - /// - /// For the pipeline, the - /// will typically contain one resource. - /// - /// The returned may be a subset - /// of the property in parameter , - /// in which case the operation of the pipeline will not be executed - /// for the omitted resources. The returned set may also contain custom - /// changes of the properties on the resources. - /// - /// If new relationships are to be created with the to-be-updated resources, - /// this will be reflected by the corresponding NavigationProperty being set. - /// For each of these relationships, the - /// hook is fired after the execution of this hook. - /// - /// If by the creation of these relationships, any other relationships (eg - /// in the case of an already populated one-to-one relationship) are implicitly - /// affected, the - /// hook is fired for these. - /// - /// The transformed resource set - /// The affected resources. - /// An enum indicating from where the hook was triggered. - IEnumerable BeforeUpdate(IDiffableResourceHashSet resources, ResourcePipeline pipeline); - - /// - /// Implement this hook to run custom logic in the - /// layer just before updating relationships to resources of type . - /// - /// This hook is fired when a relationship is created to resources of type - /// from a dependent pipeline ( - /// or ). For example, If an Article was created - /// and its author relationship was set to an existing Person, this hook will be fired - /// for that particular Person. - /// - /// The returned may be a subset - /// of , in which case the operation of the - /// pipeline will not be executed for any resource whose id was omitted - /// - /// - /// The transformed set of ids - /// The unique set of ids - /// An enum indicating from where the hook was triggered. - /// A helper that groups the resources by the affected relationship - IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline); - - /// - /// Implement this hook to run custom logic in the - /// layer just after updating resources of type . - /// - /// If relationships were updated with the updated resources, this will - /// be reflected by the corresponding NavigationProperty being set. - /// For each of these relationships, the - /// hook is fired after the execution of this hook. - /// - /// The unique set of affected resources. - /// An enum indicating from where the hook was triggered. - void AfterUpdate(HashSet resources, ResourcePipeline pipeline); - - /// - /// Implement this hook to run custom logic in the layer - /// just after a relationship was updated. - /// - /// Relationship helper. - /// An enum indicating from where the hook was triggered. - void AfterUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline); - - /// - /// Implement this hook to run custom logic in the - /// layer just before implicitly updating relationships to resources of type . - /// - /// This hook is fired when a relationship to resources of type - /// is implicitly affected from a dependent pipeline ( - /// or ). For example, if an Article was updated - /// by setting its author relationship (one-to-one) to an existing Person, - /// and by this the relationship to a different Person was implicitly removed, - /// this hook will be fired for the latter Person. - /// - /// See for information about - /// when this hook is fired. - /// - /// - /// The transformed set of ids - /// A helper that groups the resources by the affected relationship - /// An enum indicating from where the hook was triggered. - void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline); - } - - /// - /// Delete hooks container - /// - public interface IDeleteHookContainer where TResource : class, IIdentifiable - { - /// - /// Implement this hook to run custom logic in the - /// layer just before deleting resources of type . - /// - /// For the pipeline, - /// will typically contain one resource. - /// - /// The returned may be a subset - /// of , in which case the operation of the - /// pipeline will not be executed for the omitted resources. - /// - /// If by the deletion of these resources any other resources are affected - /// implicitly by the removal of their relationships (eg - /// in the case of an one-to-one relationship), the - /// hook is fired for these resources. - /// - /// The transformed resource set - /// The unique set of affected resources. - /// An enum indicating from where the hook was triggered. - IEnumerable BeforeDelete(IResourceHashSet resources, ResourcePipeline pipeline); - - /// - /// Implement this hook to run custom logic in the - /// layer just after deletion of resources of type . - /// - /// The unique set of affected resources. - /// An enum indicating from where the hook was triggered. - /// If set to true the deletion succeeded in the repository layer. - void AfterDelete(HashSet resources, ResourcePipeline pipeline, bool succeeded); - } - - /// - /// On return hook container - /// - public interface IOnReturnHookContainer where TResource : class, IIdentifiable - { - /// - /// Implement this hook to transform the result data just before returning - /// the resources of type from the - /// layer - /// - /// The returned may be a subset - /// of and may contain changes in properties - /// of the encapsulated resources. - /// - /// - /// The transformed resource set - /// The unique set of affected resources - /// An enum indicating from where the hook was triggered. - IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline); - } -} diff --git a/src/JsonApiDotNetCore/Hooks/IResourceHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/IResourceHookExecutor.cs deleted file mode 100644 index c905395286..0000000000 --- a/src/JsonApiDotNetCore/Hooks/IResourceHookExecutor.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Hooks -{ - /// - /// Transient service responsible for executing Resource Hooks as defined - /// in . see methods in - /// , and - /// for more information. - /// - /// Uses for traversal of nested resource data structures. - /// Uses for retrieving meta data about hooks, - /// fetching database values and performing other recurring internal operations. - /// - public interface IResourceHookExecutor : IReadHookExecutor, IUpdateHookExecutor, ICreateHookExecutor, IDeleteHookExecutor, IOnReturnHookExecutor { } - - public interface ICreateHookExecutor - { - /// - /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. - /// The returned set will be used in the actual operation in . - /// - /// Fires the - /// hook where T = for values in parameter . - /// - /// Fires the - /// hook for any secondary (nested) resource for values within parameter - /// - /// The transformed set - /// Target resources for the Before cycle. - /// An enum indicating from where the hook was triggered. - /// The type of the root resources - IEnumerable BeforeCreate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; - /// - /// Executes the After Cycle by firing the appropriate hooks if they are implemented. - /// - /// Fires the - /// hook where T = for values in parameter . - /// - /// Fires the - /// hook for any secondary (nested) resource for values within parameter - /// - /// Target resources for the Before cycle. - /// An enum indicating from where the hook was triggered. - /// The type of the root resources - void AfterCreate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; - } - - public interface IDeleteHookExecutor - { - /// - /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. - /// The returned set will be used in the actual operation in . - /// - /// Fires the - /// hook where T = for values in parameter . - /// - /// Fires the - /// hook for any resources that are indirectly (implicitly) affected by this operation. - /// Eg: when deleting a resource that has relationships set to other resources, - /// these other resources are implicitly affected by the delete operation. - /// - /// The transformed set - /// Target resources for the Before cycle. - /// An enum indicating from where the hook was triggered. - /// The type of the root resources - IEnumerable BeforeDelete(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; - - /// - /// Executes the After Cycle by firing the appropriate hooks if they are implemented. - /// - /// Fires the - /// hook where T = for values in parameter . - /// - /// Target resources for the Before cycle. - /// An enum indicating from where the hook was triggered. - /// If set to true the deletion succeeded. - /// The type of the root resources - void AfterDelete(IEnumerable resources, ResourcePipeline pipeline, bool succeeded) where TResource : class, IIdentifiable; - } - - /// - /// Wrapper interface for all Before execution methods. - /// - public interface IReadHookExecutor - { - /// - /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. - /// - /// Fires the - /// hook where T = for the requested - /// resources as well as any related relationship. - /// - /// An enum indicating from where the hook was triggered. - /// StringId of the requested resource in the case of - /// . - /// The type of the request resource - void BeforeRead(ResourcePipeline pipeline, string stringId = null) where TResource : class, IIdentifiable; - /// - /// Executes the After Cycle by firing the appropriate hooks if they are implemented. - /// - /// Fires the for every unique - /// resource type occuring in parameter . - /// - /// Target resources for the Before cycle. - /// An enum indicating from where the hook was triggered. - /// The type of the root resources - void AfterRead(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; - } - - /// - /// Wrapper interface for all After execution methods. - /// - public interface IUpdateHookExecutor - { - /// - /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. - /// The returned set will be used in the actual operation in . - /// - /// Fires the - /// hook where T = for values in parameter . - /// - /// Fires the - /// hook for any secondary (nested) resource for values within parameter - /// - /// Fires the - /// hook for any resources that are indirectly (implicitly) affected by this operation. - /// Eg: when updating a one-to-one relationship of a resource which already - /// had this relationship populated, then this update will indirectly affect - /// the existing relationship value. - /// - /// The transformed set - /// Target resources for the Before cycle. - /// An enum indicating from where the hook was triggered. - /// The type of the root resources - IEnumerable BeforeUpdate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; - /// - /// Executes the After Cycle by firing the appropriate hooks if they are implemented. - /// - /// Fires the - /// hook where T = for values in parameter . - /// - /// Fires the - /// hook for any secondary (nested) resource for values within parameter - /// - /// Target resources for the Before cycle. - /// An enum indicating from where the hook was triggered. - /// The type of the root resources - void AfterUpdate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; - } - - /// - /// Wrapper interface for all On execution methods. - /// - public interface IOnReturnHookExecutor - { - /// - /// Executes the On Cycle by firing the appropriate hooks if they are implemented. - /// - /// Fires the for every unique - /// resource type occuring in parameter . - /// - /// The transformed set - /// Target resources for the Before cycle. - /// An enum indicating from where the hook was triggered. - /// The type of the root resources - IEnumerable OnReturn(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; - } -} diff --git a/src/JsonApiDotNetCore/Hooks/Discovery/HooksDiscovery.cs b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/HooksDiscovery.cs similarity index 90% rename from src/JsonApiDotNetCore/Hooks/Discovery/HooksDiscovery.cs rename to src/JsonApiDotNetCore/Hooks/Internal/Discovery/HooksDiscovery.cs index 86166600e4..0c87bbfa2b 100644 --- a/src/JsonApiDotNetCore/Hooks/Discovery/HooksDiscovery.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/HooksDiscovery.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Resources; using Microsoft.Extensions.DependencyInjection; -namespace JsonApiDotNetCore.Hooks +namespace JsonApiDotNetCore.Hooks.Internal.Discovery { /// /// The default implementation for IHooksDiscovery @@ -21,7 +22,7 @@ public class HooksDiscovery : IHooksDiscovery where TResou ResourceHook.BeforeDelete }; - /// + /// public ResourceHook[] ImplementedHooks { get; private set; } public ResourceHook[] DatabaseValuesEnabledHooks { get; private set; } public ResourceHook[] DatabaseValuesDisabledHooks { get; private set; } @@ -69,7 +70,7 @@ private void DiscoverImplementedHooks(Type containerType) { if (!_databaseValuesAttributeAllowed.Contains(hook)) { - throw new JsonApiSetupException($"{nameof(LoadDatabaseValuesAttribute)} cannot be used on hook" + + throw new InvalidConfigurationException($"{nameof(LoadDatabaseValuesAttribute)} cannot be used on hook" + $"{hook:G} in resource definition {containerType.Name}"); } var targetList = attr.Value ? databaseValuesEnabledHooks : databaseValuesDisabledHooks; diff --git a/src/JsonApiDotNetCore/Hooks/Discovery/IHooksDiscovery.cs b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/IHooksDiscovery.cs similarity index 84% rename from src/JsonApiDotNetCore/Hooks/Discovery/IHooksDiscovery.cs rename to src/JsonApiDotNetCore/Hooks/Internal/Discovery/IHooksDiscovery.cs index e15c8d1404..9473ddbb24 100644 --- a/src/JsonApiDotNetCore/Hooks/Discovery/IHooksDiscovery.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/IHooksDiscovery.cs @@ -1,6 +1,7 @@ -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Resources; -namespace JsonApiDotNetCore.Hooks +namespace JsonApiDotNetCore.Hooks.Internal.Discovery { /// /// A singleton service for a particular TResource that stores a field of diff --git a/src/JsonApiDotNetCore/Hooks/Discovery/LoadDatabaseValuesAttribute.cs b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/LoadDatabaseValuesAttribute.cs similarity index 84% rename from src/JsonApiDotNetCore/Hooks/Discovery/LoadDatabaseValuesAttribute.cs rename to src/JsonApiDotNetCore/Hooks/Internal/Discovery/LoadDatabaseValuesAttribute.cs index 3caf4b5ef6..09a5393e3b 100644 --- a/src/JsonApiDotNetCore/Hooks/Discovery/LoadDatabaseValuesAttribute.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/LoadDatabaseValuesAttribute.cs @@ -1,5 +1,6 @@ using System; -namespace JsonApiDotNetCore.Hooks + +namespace JsonApiDotNetCore.Hooks.Internal.Discovery { [AttributeUsage(AttributeTargets.Method)] public sealed class LoadDatabaseValuesAttribute : Attribute diff --git a/src/JsonApiDotNetCore/Hooks/Execution/DiffableResourceHashSet.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/DiffableResourceHashSet.cs similarity index 62% rename from src/JsonApiDotNetCore/Hooks/Execution/DiffableResourceHashSet.cs rename to src/JsonApiDotNetCore/Hooks/Internal/Execution/DiffableResourceHashSet.cs index e64065a357..d555e0d412 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/DiffableResourceHashSet.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/DiffableResourceHashSet.cs @@ -4,32 +4,12 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Hooks.Internal.Discovery; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Hooks +namespace JsonApiDotNetCore.Hooks.Internal.Execution { - /// - /// A wrapper class that contains information about the resources that are updated by the request. - /// Contains the resources from the request and the corresponding database values. - /// - /// Also contains information about updated relationships through - /// implementation of IRelationshipsDictionary> - /// - public interface IDiffableResourceHashSet : IResourceHashSet where TResource : class, IIdentifiable - { - /// - /// Iterates over diffs, which is the affected resource from the request - /// with their associated current value from the database. - /// - IEnumerable> GetDiffs(); - - } - - /// public sealed class DiffableResourceHashSet : ResourceHashSet, IDiffableResourceHashSet where TResource : class, IIdentifiable { private readonly HashSet _databaseValues; @@ -76,12 +56,12 @@ public IEnumerable> GetDiffs() { var propertyInfo = TypeHelper.ParseNavigationExpression(navigationAction); var propertyType = propertyInfo.PropertyType; - if (propertyType.IsOrImplementsInterface(typeof(IEnumerable))) + if (TypeHelper.IsOrImplementsInterface(propertyType, typeof(IEnumerable))) { propertyType = TypeHelper.TryGetCollectionElementType(propertyType); } - if (propertyType.IsOrImplementsInterface(typeof(IIdentifiable))) + if (TypeHelper.IsOrImplementsInterface(propertyType, typeof(IIdentifiable))) { // the navigation action references a relationship. Redirect the call to the relationship dictionary. return base.GetAffected(navigationAction); @@ -98,26 +78,4 @@ private void ThrowNoDbValuesError() throw new MemberAccessException($"Cannot iterate over the diffs if the ${nameof(LoadDatabaseValuesAttribute)} option is set to false"); } } - - /// - /// A wrapper that contains a resource that is affected by the request, - /// matched to its current database value - /// - public sealed class ResourceDiffPair where TResource : class, IIdentifiable - { - public ResourceDiffPair(TResource resource, TResource databaseValue) - { - Resource = resource; - DatabaseValue = databaseValue; - } - - /// - /// The resource from the request matching the resource from the database. - /// - public TResource Resource { get; } - /// - /// The resource from the database matching the resource from the request. - /// - public TResource DatabaseValue { get; } - } } diff --git a/src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/HookExecutorHelper.cs similarity index 91% rename from src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs rename to src/JsonApiDotNetCore/Hooks/Internal/Execution/HookExecutorHelper.cs index 56491b4ad4..03c8ffa8d5 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/HookExecutorHelper.cs @@ -3,22 +3,19 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Internal.Generics; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Extensions; -using LeftType = System.Type; -using RightType = System.Type; -using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Hooks.Internal.Discovery; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using LeftType = System.Type; +using RightType = System.Type; -namespace JsonApiDotNetCore.Hooks +namespace JsonApiDotNetCore.Hooks.Internal.Execution { - /// + /// internal sealed class HookExecutorHelper : IHookExecutorHelper { private readonly IdentifiableComparer _comparer = IdentifiableComparer.Instance; @@ -39,7 +36,7 @@ public HookExecutorHelper(IGenericServiceFactory genericProcessorFactory, IResou _targetedHooksForRelatedResources = new List(); } - /// + /// public IResourceHookContainer GetResourceHookContainer(RightType targetResource, ResourceHook hook = ResourceHook.None) { // checking the cache if we have a reference for the requested container, @@ -48,7 +45,7 @@ public IResourceHookContainer GetResourceHookContainer(RightType targetResource, // so we need not even bother. if (!_hookContainers.TryGetValue(targetResource, out IResourceHookContainer container)) { - container = (_genericProcessorFactory.Get(typeof(ResourceDefinition<>), targetResource)); + container = _genericProcessorFactory.Get(typeof(ResourceDefinition<>), targetResource); _hookContainers[targetResource] = container; } if (container == null) return null; @@ -73,7 +70,7 @@ public IResourceHookContainer GetResourceHookContainer(RightType targetResource, return null; } - /// + /// public IResourceHookContainer GetResourceHookContainer(ResourceHook hook = ResourceHook.None) where TResource : class, IIdentifiable { return (IResourceHookContainer)GetResourceHookContainer(typeof(TResource), hook); @@ -86,10 +83,10 @@ public IEnumerable LoadDbValues(LeftType resourceTypeForRepository, IEnumerable .GetMethod(nameof(GetWhereAndInclude), BindingFlags.NonPublic | BindingFlags.Instance) .MakeGenericMethod(resourceTypeForRepository, idType); var cast = ((IEnumerable)resources).Cast(); - var ids = cast.Select(TypeHelper.GetResourceTypedId).CopyToList(idType); + var ids = TypeHelper.CopyToList(cast.Select(TypeHelper.GetResourceTypedId), idType); var values = (IEnumerable)parameterizedGetWhere.Invoke(this, new object[] { ids, relationshipsToNextLayer }); if (values == null) return null; - return (IEnumerable)Activator.CreateInstance(typeof(HashSet<>).MakeGenericType(resourceTypeForRepository), values.CopyToList(resourceTypeForRepository)); + return (IEnumerable)Activator.CreateInstance(typeof(HashSet<>).MakeGenericType(resourceTypeForRepository), TypeHelper.CopyToList(values, resourceTypeForRepository)); } public HashSet LoadDbValues(IEnumerable resources, ResourceHook hook, params RelationshipAttribute[] relationships) where TResource : class, IIdentifiable @@ -197,7 +194,11 @@ public Dictionary LoadImplicitlyAffected( affected = TypeHelper.CreateListFor(relationship.RightType); implicitlyAffected[relationship] = affected; } - ((IList)affected).AddRange(dbRightResourceListCast); + + foreach (var item in dbRightResourceListCast) + { + ((IList)affected).Add(item); + } } } } @@ -210,8 +211,8 @@ private bool IsHasManyThrough(KeyValuePair k out RelationshipAttribute attr) { attr = kvp.Key; - resources = (kvp.Value); - return (kvp.Key is HasManyThroughAttribute); + resources = kvp.Value; + return kvp.Key is HasManyThroughAttribute; } } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IByAffectedRelationships.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IByAffectedRelationships.cs new file mode 100644 index 0000000000..689ac13260 --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IByAffectedRelationships.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCore.Hooks.Internal.Execution +{ + /// + /// An interface that is implemented to expose a relationship dictionary on another class. + /// + public interface IByAffectedRelationships : + IRelationshipGetters where TRightResource : class, IIdentifiable + { + /// + /// Gets a dictionary of affected resources grouped by affected relationships. + /// + Dictionary> AffectedRelationships { get; } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IDiffableResourceHashSet.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IDiffableResourceHashSet.cs new file mode 100644 index 0000000000..1a7d98fc14 --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IDiffableResourceHashSet.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Hooks.Internal.Execution +{ + /// + /// A wrapper class that contains information about the resources that are updated by the request. + /// Contains the resources from the request and the corresponding database values. + /// + /// Also contains information about updated relationships through + /// implementation of IRelationshipsDictionary> + /// + public interface IDiffableResourceHashSet : IResourceHashSet where TResource : class, IIdentifiable + { + /// + /// Iterates over diffs, which is the affected resource from the request + /// with their associated current value from the database. + /// + IEnumerable> GetDiffs(); + + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Hooks/Execution/IHookExecutorHelper.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IHookExecutorHelper.cs similarity index 95% rename from src/JsonApiDotNetCore/Hooks/Execution/IHookExecutorHelper.cs rename to src/JsonApiDotNetCore/Hooks/Internal/Execution/IHookExecutorHelper.cs index 9b37428e8f..b04ef1aae5 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/IHookExecutorHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IHookExecutorHelper.cs @@ -1,10 +1,10 @@ using System; using System.Collections; using System.Collections.Generic; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Hooks +namespace JsonApiDotNetCore.Hooks.Internal.Execution { /// /// A helper class for retrieving meta data about hooks, diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipGetters.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipGetters.cs new file mode 100644 index 0000000000..ca95ddef87 --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipGetters.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCore.Hooks.Internal.Execution +{ + /// + /// A helper class that provides insights in which relationships have been updated for which resources. + /// + public interface IRelationshipGetters where TLeftResource : class, IIdentifiable + { + /// + /// Gets a dictionary of all resources that have an affected relationship to type + /// + Dictionary> GetByRelationship() where TRightResource : class, IIdentifiable; + /// + /// Gets a dictionary of all resources that have an affected relationship to type + /// + Dictionary> GetByRelationship(Type relatedResourceType); + /// + /// Gets a collection of all the resources for the property within + /// has been affected by the request + /// + HashSet GetAffected(Expression> navigationAction); + } +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipsDictionary.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipsDictionary.cs new file mode 100644 index 0000000000..1ddd5cbc36 --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipsDictionary.cs @@ -0,0 +1,7 @@ +namespace JsonApiDotNetCore.Hooks.Internal.Execution +{ + /// + /// A dummy interface used internally by the hook executor. + /// + public interface IRelationshipsDictionary { } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IResourceHashSet.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IResourceHashSet.cs new file mode 100644 index 0000000000..8863ac4686 --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IResourceHashSet.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Hooks.Internal.Execution +{ + /// + /// Basically a enumerable of of resources that were affected by the request. + /// + /// Also contains information about updated relationships through + /// implementation of IAffectedRelationshipsDictionary> + /// + public interface IResourceHashSet : IByAffectedRelationships, IReadOnlyCollection where TResource : class, IIdentifiable { } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Hooks/Execution/RelationshipsDictionary.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/RelationshipsDictionary.cs similarity index 54% rename from src/JsonApiDotNetCore/Hooks/Execution/RelationshipsDictionary.cs rename to src/JsonApiDotNetCore/Hooks/Internal/Execution/RelationshipsDictionary.cs index 3f9ec5e7ad..f624b93aad 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/RelationshipsDictionary.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/RelationshipsDictionary.cs @@ -3,29 +3,11 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Hooks +namespace JsonApiDotNetCore.Hooks.Internal.Execution { - /// - /// A dummy interface used internally by the hook executor. - /// - public interface IRelationshipsDictionary { } - - /// - /// An interface that is implemented to expose a relationship dictionary on another class. - /// - public interface IByAffectedRelationships : - IRelationshipGetters where TRightResource : class, IIdentifiable - { - /// - /// Gets a dictionary of affected resources grouped by affected relationships. - /// - Dictionary> AffectedRelationships { get; } - } - /// /// A helper class that provides insights in which relationships have been updated for which resources. /// @@ -35,27 +17,6 @@ public interface IRelationshipsDictionary : IRelationshipsDictionary where TRightResource : class, IIdentifiable { } - /// - /// A helper class that provides insights in which relationships have been updated for which resources. - /// - public interface IRelationshipGetters where TLeftResource : class, IIdentifiable - { - /// - /// Gets a dictionary of all resources that have an affected relationship to type - /// - Dictionary> GetByRelationship() where TRightResource : class, IIdentifiable; - /// - /// Gets a dictionary of all resources that have an affected relationship to type - /// - Dictionary> GetByRelationship(Type relatedResourceType); - /// - /// Gets a collection of all the resources for the property within - /// has been affected by the request - /// - /// - HashSet GetAffected(Expression> navigationAction); - } - /// /// Implementation of IAffectedRelationships{TRightResource} @@ -68,13 +29,13 @@ public class RelationshipsDictionary : IRelationshipsDictionary where TResource : class, IIdentifiable { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Relationships. public RelationshipsDictionary(Dictionary> relationships) : base(relationships) { } /// - /// Used internally by the ResourceHookExecutor to make live a bit easier with generics + /// Used internally by the ResourceHookExecutor to make life a bit easier with generics /// internal RelationshipsDictionary(Dictionary relationships) : this(TypeHelper.ConvertRelationshipDictionary(relationships)) { } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceDiffPair.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceDiffPair.cs new file mode 100644 index 0000000000..a627c5d38b --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceDiffPair.cs @@ -0,0 +1,26 @@ +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Hooks.Internal.Execution +{ + /// + /// A wrapper that contains a resource that is affected by the request, + /// matched to its current database value + /// + public sealed class ResourceDiffPair where TResource : class, IIdentifiable + { + public ResourceDiffPair(TResource resource, TResource databaseValue) + { + Resource = resource; + DatabaseValue = databaseValue; + } + + /// + /// The resource from the request matching the resource from the database. + /// + public TResource Resource { get; } + /// + /// The resource from the database matching the resource from the request. + /// + public TResource DatabaseValue { get; } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Hooks/Execution/ResourceHashSet.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHashSet.cs similarity index 77% rename from src/JsonApiDotNetCore/Hooks/Execution/ResourceHashSet.cs rename to src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHashSet.cs index 70942284e1..9b61f57d36 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/ResourceHashSet.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHashSet.cs @@ -1,21 +1,12 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Models; -using System.Collections; -using JsonApiDotNetCore.Internal; using System; +using System.Collections; +using System.Collections.Generic; using System.Linq.Expressions; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Hooks +namespace JsonApiDotNetCore.Hooks.Internal.Execution { - /// - /// Basically a enumerable of of resources that were affected by the request. - /// - /// Also contains information about updated relationships through - /// implementation of IAffectedRelationshipsDictionary> - /// - public interface IResourceHashSet : IByAffectedRelationships, IReadOnlyCollection where TResource : class, IIdentifiable { } - /// /// Implementation of IResourceHashSet{TResource}. /// diff --git a/src/JsonApiDotNetCore/Hooks/Execution/ResourceHookEnum.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHook.cs similarity index 90% rename from src/JsonApiDotNetCore/Hooks/Execution/ResourceHookEnum.cs rename to src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHook.cs index 9a22e527b1..f60978586f 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/ResourceHookEnum.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHook.cs @@ -1,4 +1,4 @@ -namespace JsonApiDotNetCore.Hooks +namespace JsonApiDotNetCore.Hooks.Internal.Execution { /// diff --git a/src/JsonApiDotNetCore/Hooks/Execution/ResourcePipelineEnum.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourcePipeline.cs similarity index 90% rename from src/JsonApiDotNetCore/Hooks/Execution/ResourcePipelineEnum.cs rename to src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourcePipeline.cs index 1c86237b59..ffeb61a07f 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/ResourcePipelineEnum.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourcePipeline.cs @@ -1,6 +1,6 @@ using JsonApiDotNetCore.Services; -namespace JsonApiDotNetCore.Hooks +namespace JsonApiDotNetCore.Hooks.Internal.Execution { /// /// An enum that represents the initiator of a resource hook. Eg, when BeforeCreate() diff --git a/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookContainer.cs new file mode 100644 index 0000000000..4a0369617b --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookContainer.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Hooks.Internal +{ + /// + /// Create hooks container + /// + public interface ICreateHookContainer where TResource : class, IIdentifiable + { + /// + /// Implement this hook to run custom logic in the + /// layer just before creation of resources of type . + /// + /// For the pipeline, + /// will typically contain one entry. + /// + /// The returned may be a subset + /// of , in which case the operation of the + /// pipeline will not be executed for the omitted resources. The returned + /// set may also contain custom changes of the properties on the resources. + /// + /// If new relationships are to be created with the to-be-created resources, + /// this will be reflected by the corresponding NavigationProperty being set. + /// For each of these relationships, the + /// hook is fired after the execution of this hook. + /// + /// The transformed resource set + /// The unique set of affected resources. + /// An enum indicating from where the hook was triggered. + IEnumerable BeforeCreate(IResourceHashSet resources, ResourcePipeline pipeline); + + /// + /// Implement this hook to run custom logic in the + /// layer just after creation of resources of type . + /// + /// If relationships were created with the created resources, this will + /// be reflected by the corresponding NavigationProperty being set. + /// For each of these relationships, the + /// hook is fired after the execution of this hook. + /// + /// The transformed resource set + /// The unique set of affected resources. + /// An enum indicating from where the hook was triggered. + void AfterCreate(HashSet resources, ResourcePipeline pipeline); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookExecutor.cs new file mode 100644 index 0000000000..e2f90560fb --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookExecutor.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Hooks.Internal +{ + public interface ICreateHookExecutor + { + /// + /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. + /// The returned set will be used in the actual operation in . + /// + /// Fires the + /// hook where T = for values in parameter . + /// + /// Fires the + /// hook for any secondary (nested) resource for values within parameter + /// + /// The transformed set + /// Target resources for the Before cycle. + /// An enum indicating from where the hook was triggered. + /// The type of the root resources + IEnumerable BeforeCreate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + /// + /// Executes the After Cycle by firing the appropriate hooks if they are implemented. + /// + /// Fires the + /// hook where T = for values in parameter . + /// + /// Fires the + /// hook for any secondary (nested) resource for values within parameter + /// + /// Target resources for the Before cycle. + /// An enum indicating from where the hook was triggered. + /// The type of the root resources + void AfterCreate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + } +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookContainer.cs new file mode 100644 index 0000000000..9f9a8c41d5 --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookContainer.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Hooks.Internal +{ + /// + /// Delete hooks container + /// + public interface IDeleteHookContainer where TResource : class, IIdentifiable + { + /// + /// Implement this hook to run custom logic in the + /// layer just before deleting resources of type . + /// + /// For the pipeline, + /// will typically contain one resource. + /// + /// The returned may be a subset + /// of , in which case the operation of the + /// pipeline will not be executed for the omitted resources. + /// + /// If by the deletion of these resources any other resources are affected + /// implicitly by the removal of their relationships (eg + /// in the case of an one-to-one relationship), the + /// hook is fired for these resources. + /// + /// The transformed resource set + /// The unique set of affected resources. + /// An enum indicating from where the hook was triggered. + IEnumerable BeforeDelete(IResourceHashSet resources, ResourcePipeline pipeline); + + /// + /// Implement this hook to run custom logic in the + /// layer just after deletion of resources of type . + /// + /// The unique set of affected resources. + /// An enum indicating from where the hook was triggered. + /// If set to true the deletion succeeded in the repository layer. + void AfterDelete(HashSet resources, ResourcePipeline pipeline, bool succeeded); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookExecutor.cs new file mode 100644 index 0000000000..205d2cacc6 --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookExecutor.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Hooks.Internal +{ + public interface IDeleteHookExecutor + { + /// + /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. + /// The returned set will be used in the actual operation in . + /// + /// Fires the + /// hook where T = for values in parameter . + /// + /// Fires the + /// hook for any resources that are indirectly (implicitly) affected by this operation. + /// Eg: when deleting a resource that has relationships set to other resources, + /// these other resources are implicitly affected by the delete operation. + /// + /// The transformed set + /// Target resources for the Before cycle. + /// An enum indicating from where the hook was triggered. + /// The type of the root resources + IEnumerable BeforeDelete(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + + /// + /// Executes the After Cycle by firing the appropriate hooks if they are implemented. + /// + /// Fires the + /// hook where T = for values in parameter . + /// + /// Target resources for the Before cycle. + /// An enum indicating from where the hook was triggered. + /// If set to true the deletion succeeded. + /// The type of the root resources + void AfterDelete(IEnumerable resources, ResourcePipeline pipeline, bool succeeded) where TResource : class, IIdentifiable; + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookContainer.cs new file mode 100644 index 0000000000..61db0db85f --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookContainer.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Hooks.Internal +{ + /// + /// On return hook container + /// + public interface IOnReturnHookContainer where TResource : class, IIdentifiable + { + /// + /// Implement this hook to transform the result data just before returning + /// the resources of type from the + /// layer + /// + /// The returned may be a subset + /// of and may contain changes in properties + /// of the encapsulated resources. + /// + /// + /// The transformed resource set + /// The unique set of affected resources + /// An enum indicating from where the hook was triggered. + IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookExecutor.cs new file mode 100644 index 0000000000..3bea4012fa --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookExecutor.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Hooks.Internal +{ + /// + /// Wrapper interface for all On execution methods. + /// + public interface IOnReturnHookExecutor + { + /// + /// Executes the On Cycle by firing the appropriate hooks if they are implemented. + /// + /// Fires the for every unique + /// resource type occurring in parameter . + /// + /// The transformed set + /// Target resources for the Before cycle. + /// An enum indicating from where the hook was triggered. + /// The type of the root resources + IEnumerable OnReturn(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + } +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IReadHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/IReadHookContainer.cs new file mode 100644 index 0000000000..9913f8f2d4 --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/IReadHookContainer.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Hooks.Internal +{ + /// + /// Read hooks container + /// + public interface IReadHookContainer where TResource : class, IIdentifiable + { + /// + /// Implement this hook to run custom logic in the + /// layer just before reading resources of type . + /// + /// An enum indicating from where the hook was triggered. + /// Indicates whether the to be queried resources are the primary request resources or if they were included + /// The string ID of the requested resource, in the case of + void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null); + /// + /// Implement this hook to run custom logic in the + /// layer just after reading resources of type . + /// + /// The unique set of affected resources. + /// An enum indicating from where the hook was triggered. + /// A boolean to indicate whether the resources in this hook execution are the primary resources of the request, + /// or if they were included as a relationship + void AfterRead(HashSet resources, ResourcePipeline pipeline, bool isIncluded = false); + } +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IReadHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IReadHookExecutor.cs new file mode 100644 index 0000000000..0abbbe583f --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/IReadHookExecutor.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Hooks.Internal +{ + /// + /// Wrapper interface for all Before execution methods. + /// + public interface IReadHookExecutor + { + /// + /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. + /// + /// Fires the + /// hook where T = for the requested + /// resources as well as any related relationship. + /// + /// An enum indicating from where the hook was triggered. + /// StringId of the requested resource in the case of + /// . + /// The type of the request resource + void BeforeRead(ResourcePipeline pipeline, string stringId = null) where TResource : class, IIdentifiable; + /// + /// Executes the After Cycle by firing the appropriate hooks if they are implemented. + /// + /// Fires the for every unique + /// resource type occurring in parameter . + /// + /// Target resources for the Before cycle. + /// An enum indicating from where the hook was triggered. + /// The type of the root resources + void AfterRead(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + } +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookContainer.cs new file mode 100644 index 0000000000..60e0913fa4 --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookContainer.cs @@ -0,0 +1,17 @@ +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Hooks.Internal +{ + /// + /// Not meant for public usage. Used internally in the + /// + public interface IResourceHookContainer { } + + /// + /// Implement this interface to implement business logic hooks on . + /// + public interface IResourceHookContainer + : IReadHookContainer, IDeleteHookContainer, ICreateHookContainer, + IUpdateHookContainer, IOnReturnHookContainer, IResourceHookContainer + where TResource : class, IIdentifiable { } +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookExecutor.cs new file mode 100644 index 0000000000..3faa9f7798 --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookExecutor.cs @@ -0,0 +1,18 @@ +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Hooks.Internal.Traversal; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Hooks.Internal +{ + /// + /// Transient service responsible for executing Resource Hooks as defined + /// in . see methods in + /// , and + /// for more information. + /// + /// Uses for traversal of nested resource data structures. + /// Uses for retrieving meta data about hooks, + /// fetching database values and performing other recurring internal operations. + /// + public interface IResourceHookExecutor : IReadHookExecutor, IUpdateHookExecutor, ICreateHookExecutor, IDeleteHookExecutor, IOnReturnHookExecutor { } +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookContainer.cs new file mode 100644 index 0000000000..f0aa20df5a --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookContainer.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Hooks.Internal +{ + /// + /// update hooks container + /// + public interface IUpdateHookContainer where TResource : class, IIdentifiable + { + /// + /// Implement this hook to run custom logic in the + /// layer just before updating resources of type . + /// + /// For the pipeline, the + /// will typically contain one resource. + /// + /// The returned may be a subset + /// of the property in parameter , + /// in which case the operation of the pipeline will not be executed + /// for the omitted resources. The returned set may also contain custom + /// changes of the properties on the resources. + /// + /// If new relationships are to be created with the to-be-updated resources, + /// this will be reflected by the corresponding NavigationProperty being set. + /// For each of these relationships, the + /// hook is fired after the execution of this hook. + /// + /// If by the creation of these relationships, any other relationships (eg + /// in the case of an already populated one-to-one relationship) are implicitly + /// affected, the + /// hook is fired for these. + /// + /// The transformed resource set + /// The affected resources. + /// An enum indicating from where the hook was triggered. + IEnumerable BeforeUpdate(IDiffableResourceHashSet resources, ResourcePipeline pipeline); + + /// + /// Implement this hook to run custom logic in the + /// layer just before updating relationships to resources of type . + /// + /// This hook is fired when a relationship is created to resources of type + /// from a dependent pipeline ( + /// or ). For example, If an Article was created + /// and its author relationship was set to an existing Person, this hook will be fired + /// for that particular Person. + /// + /// The returned may be a subset + /// of , in which case the operation of the + /// pipeline will not be executed for any resource whose ID was omitted + /// + /// + /// The transformed set of ids + /// The unique set of ids + /// An enum indicating from where the hook was triggered. + /// A helper that groups the resources by the affected relationship + IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline); + + /// + /// Implement this hook to run custom logic in the + /// layer just after updating resources of type . + /// + /// If relationships were updated with the updated resources, this will + /// be reflected by the corresponding NavigationProperty being set. + /// For each of these relationships, the + /// hook is fired after the execution of this hook. + /// + /// The unique set of affected resources. + /// An enum indicating from where the hook was triggered. + void AfterUpdate(HashSet resources, ResourcePipeline pipeline); + + /// + /// Implement this hook to run custom logic in the layer + /// just after a relationship was updated. + /// + /// Relationship helper. + /// An enum indicating from where the hook was triggered. + void AfterUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline); + + /// + /// Implement this hook to run custom logic in the + /// layer just before implicitly updating relationships to resources of type . + /// + /// This hook is fired when a relationship to resources of type + /// is implicitly affected from a dependent pipeline ( + /// or ). For example, if an Article was updated + /// by setting its author relationship (one-to-one) to an existing Person, + /// and by this the relationship to a different Person was implicitly removed, + /// this hook will be fired for the latter Person. + /// + /// See for information about + /// when this hook is fired. + /// + /// + /// The transformed set of ids + /// A helper that groups the resources by the affected relationship + /// An enum indicating from where the hook was triggered. + void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline); + } +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookExecutor.cs new file mode 100644 index 0000000000..ba1320a03e --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookExecutor.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Hooks.Internal +{ + /// + /// Wrapper interface for all After execution methods. + /// + public interface IUpdateHookExecutor + { + /// + /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. + /// The returned set will be used in the actual operation in . + /// + /// Fires the + /// hook where T = for values in parameter . + /// + /// Fires the + /// hook for any secondary (nested) resource for values within parameter + /// + /// Fires the + /// hook for any resources that are indirectly (implicitly) affected by this operation. + /// Eg: when updating a one-to-one relationship of a resource which already + /// had this relationship populated, then this update will indirectly affect + /// the existing relationship value. + /// + /// The transformed set + /// Target resources for the Before cycle. + /// An enum indicating from where the hook was triggered. + /// The type of the root resources + IEnumerable BeforeUpdate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + /// + /// Executes the After Cycle by firing the appropriate hooks if they are implemented. + /// + /// Fires the + /// hook where T = for values in parameter . + /// + /// Fires the + /// hook for any secondary (nested) resource for values within parameter + /// + /// Target resources for the Before cycle. + /// An enum indicating from where the hook was triggered. + /// The type of the root resources + void AfterUpdate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IdentifiableComparer.cs b/src/JsonApiDotNetCore/Hooks/Internal/IdentifiableComparer.cs new file mode 100644 index 0000000000..4130afa92f --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/IdentifiableComparer.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Hooks.Internal +{ + /// + /// Compares `IIdentifiable` with each other based on ID + /// + internal sealed class IdentifiableComparer : IEqualityComparer + { + public static readonly IdentifiableComparer Instance = new IdentifiableComparer(); + + private IdentifiableComparer() + { + } + + public bool Equals(IIdentifiable x, IIdentifiable y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (x is null || y is null || x.GetType() != y.GetType()) + { + return false; + } + + return x.StringId == y.StringId; + } + + public int GetHashCode(IIdentifiable obj) + { + return obj.StringId != null ? obj.StringId.GetHashCode() : 0; + } + } +} diff --git a/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutor.cs similarity index 96% rename from src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs rename to src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutor.cs index c1de951efc..1c0721a256 100644 --- a/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutor.cs @@ -3,20 +3,19 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using LeftType = System.Type; -using RightType = System.Type; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Hooks.Internal.Traversal; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using LeftType = System.Type; +using RightType = System.Type; -namespace JsonApiDotNetCore.Hooks +namespace JsonApiDotNetCore.Hooks.Internal { - /// + /// internal sealed class ResourceHookExecutor : IResourceHookExecutor { private readonly IHookExecutorHelper _executorHelper; @@ -42,7 +41,7 @@ public ResourceHookExecutor( _resourceFactory = resourceFactory; } - /// + /// public void BeforeRead(ResourcePipeline pipeline, string stringId = null) where TResource : class, IIdentifiable { var hookContainer = _executorHelper.GetResourceHookContainer(ResourceHook.BeforeRead); @@ -61,7 +60,7 @@ public void BeforeRead(ResourcePipeline pipeline, string stringId = n } } - /// + /// public IEnumerable BeforeUpdate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable { if (GetHook(ResourceHook.BeforeUpdate, resources, out var container, out var node)) @@ -78,7 +77,7 @@ public IEnumerable BeforeUpdate(IEnumerable res return resources; } - /// + /// public IEnumerable BeforeCreate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable { if (GetHook(ResourceHook.BeforeCreate, resources, out var container, out var node)) @@ -92,7 +91,7 @@ public IEnumerable BeforeCreate(IEnumerable res return resources; } - /// + /// public IEnumerable BeforeDelete(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable { if (GetHook(ResourceHook.BeforeDelete, resources, out var container, out var node)) @@ -119,7 +118,7 @@ public IEnumerable BeforeDelete(IEnumerable res return resources; } - /// + /// public IEnumerable OnReturn(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable { if (GetHook(ResourceHook.OnReturn, resources, out var container, out var node) && pipeline != ResourcePipeline.GetRelationship) @@ -139,7 +138,7 @@ public IEnumerable OnReturn(IEnumerable resourc return resources; } - /// + /// public void AfterRead(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable { if (GetHook(ResourceHook.AfterRead, resources, out var container, out var node)) @@ -153,7 +152,7 @@ public void AfterRead(IEnumerable resources, ResourcePipel }); } - /// + /// public void AfterCreate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable { if (GetHook(ResourceHook.AfterCreate, resources, out var container, out var node)) @@ -166,7 +165,7 @@ public void AfterCreate(IEnumerable resources, ResourcePip (nextContainer, nextNode) => FireAfterUpdateRelationship(nextContainer, nextNode, pipeline)); } - /// + /// public void AfterUpdate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable { if (GetHook(ResourceHook.AfterUpdate, resources, out var container, out var node)) @@ -179,7 +178,7 @@ public void AfterUpdate(IEnumerable resources, ResourcePip (nextContainer, nextNode) => FireAfterUpdateRelationship(nextContainer, nextNode, pipeline)); } - /// + /// public void AfterDelete(IEnumerable resources, ResourcePipeline pipeline, bool succeeded) where TResource : class, IIdentifiable { if (GetHook(ResourceHook.AfterDelete, resources, out var container, out var node)) @@ -271,7 +270,7 @@ private void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, NodeLayer la var dbValues = LoadDbValues(resourceType, uniqueResources, ResourceHook.BeforeUpdateRelationship, relationships); // these are the resources of the current node grouped by - // RelationshipAttributes that occured in the previous layer + // RelationshipAttributes that occurred in the previous layer // so it looks like { HasOneAttribute:owner => owner_new }. // Note that in the BeforeUpdateRelationship hook of Person, // we want want inverse relationship attribute: @@ -339,7 +338,7 @@ private Dictionary ReplaceKeysWithInverseRel // If it isn't, JADNC currently knows nothing about this relationship pointing back, and it // currently cannot fire hooks for resources resolved through inverse relationships. var inversableRelationshipAttributes = resourcesByRelationship.Where(kvp => kvp.Key.InverseNavigation != null); - return inversableRelationshipAttributes.ToDictionary(kvp => _resourceGraph.GetInverse(kvp.Key), kvp => kvp.Value); + return inversableRelationshipAttributes.ToDictionary(kvp => _resourceGraph.GetInverseRelationship(kvp.Key), kvp => kvp.Value); } /// @@ -352,7 +351,7 @@ private void FireForAffectedImplicits(Type resourceTypeToInclude, Dictionary _resourceGraph.GetInverse(kvp.Key), kvp => kvp.Value); + var inverse = implicitAffected.ToDictionary(kvp => _resourceGraph.GetInverseRelationship(kvp.Key), kvp => kvp.Value); var resourcesByRelationship = CreateRelationshipHelper(resourceTypeToInclude, inverse); CallHook(container, ResourceHook.BeforeImplicitUpdateRelationship, new object[] { resourcesByRelationship, pipeline, }); } @@ -418,14 +417,14 @@ private Dictionary ReplaceWithDbValues(Dicti { foreach (var key in prevLayerRelationships.Keys.ToList()) { - var replaced = prevLayerRelationships[key].Cast().Select(resource => dbValues.Single(dbResource => dbResource.StringId == resource.StringId)).CopyToList(key.LeftType); + var replaced = TypeHelper.CopyToList(prevLayerRelationships[key].Cast().Select(resource => dbValues.Single(dbResource => dbResource.StringId == resource.StringId)), key.LeftType); prevLayerRelationships[key] = TypeHelper.CreateHashSetFor(key.LeftType, replaced); } return prevLayerRelationships; } /// - /// Filter the source set by removing the resources with id that are not + /// Filter the source set by removing the resources with ID that are not /// in . /// private HashSet GetAllowedResources(IEnumerable source, IEnumerable allowedIds) @@ -459,7 +458,7 @@ private void FireAfterUpdateRelationship(IResourceHookContainer container, IReso Dictionary currentResourcesGrouped = node.RelationshipsFromPreviousLayer.GetRightResources(); // the relationships attributes in currentResourcesGrouped will be pointing from a - // resource in the previouslayer to a resource in the current (nested) layer. + // resource in the previous layer to a resource in the current (nested) layer. // For the nested hook we need to replace these attributes with their inverse. // See the FireNestedBeforeUpdateHooks method for a more detailed example. var resourcesByRelationship = CreateRelationshipHelper(node.ResourceType, ReplaceKeysWithInverseRelationships(currentResourcesGrouped)); diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/ChildNode.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ChildNode.cs similarity index 90% rename from src/JsonApiDotNetCore/Hooks/Traversal/ChildNode.cs rename to src/JsonApiDotNetCore/Hooks/Internal/Traversal/ChildNode.cs index 9e6e45b297..49df8d702c 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/ChildNode.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ChildNode.cs @@ -1,12 +1,10 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Resources; using RightType = System.Type; -namespace JsonApiDotNetCore.Hooks +namespace JsonApiDotNetCore.Hooks.Internal.Traversal { /// /// Child node in the tree @@ -24,7 +22,7 @@ public IEnumerable UniqueResources { get { - return new HashSet(_relationshipsFromPreviousLayer.SelectMany(rfpl => rfpl.RightResources)); + return new HashSet(_relationshipsFromPreviousLayer.SelectMany(relationshipGroup => relationshipGroup.RightResources)); } } @@ -68,7 +66,7 @@ public void Reassign(IResourceFactory resourceFactory, IEnumerable updated = nul if (currentValue is IEnumerable relationshipCollection) { var intersection = relationshipCollection.Intersect(unique, _comparer); - IEnumerable typedCollection = intersection.CopyToTypedCollection(relationshipCollection.GetType()); + IEnumerable typedCollection = TypeHelper.CopyToTypedCollection(intersection, relationshipCollection.GetType()); proxy.SetValue(left, typedCollection, resourceFactory); } else if (currentValue is IIdentifiable relationshipSingle) diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipGroup.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipGroup.cs new file mode 100644 index 0000000000..42b20ced84 --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipGroup.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Hooks.Internal.Traversal +{ + internal interface IRelationshipGroup + { + RelationshipProxy Proxy { get; } + HashSet LeftResources { get; } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipsFromPreviousLayer.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipsFromPreviousLayer.cs new file mode 100644 index 0000000000..ed7b6fdf00 --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipsFromPreviousLayer.cs @@ -0,0 +1,23 @@ +using System.Collections; +using System.Collections.Generic; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCore.Hooks.Internal.Traversal +{ + /// + /// A helper class for mapping relationships between a current and previous layer + /// + internal interface IRelationshipsFromPreviousLayer + { + /// + /// Grouped by relationship to the previous layer, gets all the resources of the current layer + /// + /// The right side resources. + Dictionary GetRightResources(); + /// + /// Grouped by relationship to the previous layer, gets all the resources of the previous layer + /// + /// The right side resources. + Dictionary GetLeftResources(); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/IResourceNode.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IResourceNode.cs similarity index 94% rename from src/JsonApiDotNetCore/Hooks/Traversal/IResourceNode.cs rename to src/JsonApiDotNetCore/Hooks/Internal/Traversal/IResourceNode.cs index 73a2a16f5d..5ae502336c 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/IResourceNode.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IResourceNode.cs @@ -1,8 +1,8 @@ using System.Collections; -using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Resources; using RightType = System.Type; -namespace JsonApiDotNetCore.Hooks +namespace JsonApiDotNetCore.Hooks.Internal.Traversal { /// /// This is the interface that nodes need to inherit from diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/ITraversalHelper.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ITraversalHelper.cs similarity index 83% rename from src/JsonApiDotNetCore/Hooks/Traversal/ITraversalHelper.cs rename to src/JsonApiDotNetCore/Hooks/Internal/Traversal/ITraversalHelper.cs index 017d3c78c1..d3e2e2ba6a 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/ITraversalHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ITraversalHelper.cs @@ -1,21 +1,17 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Resources; -namespace JsonApiDotNetCore.Hooks +namespace JsonApiDotNetCore.Hooks.Internal.Traversal { internal interface ITraversalHelper { /// /// Crates the next layer /// - /// - /// NodeLayer CreateNextLayer(IResourceNode node); /// /// Creates the next layer based on the nodes provided /// - /// - /// NodeLayer CreateNextLayer(IEnumerable nodes); /// /// Creates a root node for breadth-first-traversal (BFS). Note that typically, in diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/NodeLayer.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/NodeLayer.cs new file mode 100644 index 0000000000..711ddd28fb --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/NodeLayer.cs @@ -0,0 +1,36 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Hooks.Internal.Traversal +{ + /// + /// A helper class that represents all resources in the current layer that + /// are being traversed for which hooks will be executed (see IResourceHookExecutor) + /// + internal sealed class NodeLayer : IEnumerable + { + private readonly List _collection; + + public bool AnyResources() + { + return _collection.Any(n => n.UniqueResources.Cast().Any()); + } + + public NodeLayer(List nodes) + { + _collection = nodes; + } + + public IEnumerator GetEnumerator() + { + return _collection.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/RelationshipGroup.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipGroup.cs similarity index 73% rename from src/JsonApiDotNetCore/Hooks/Traversal/RelationshipGroup.cs rename to src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipGroup.cs index c6771c1ad6..12ac8382cd 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/RelationshipGroup.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipGroup.cs @@ -1,14 +1,8 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Resources; -namespace JsonApiDotNetCore.Hooks +namespace JsonApiDotNetCore.Hooks.Internal.Traversal { - internal interface IRelationshipGroup - { - RelationshipProxy Proxy { get; } - HashSet LeftResources { get; } - } - internal sealed class RelationshipGroup : IRelationshipGroup where TRight : class, IIdentifiable { public RelationshipProxy Proxy { get; } diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/RelationshipProxy.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipProxy.cs similarity index 90% rename from src/JsonApiDotNetCore/Hooks/Traversal/RelationshipProxy.cs rename to src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipProxy.cs index 62bacec551..ab701882e5 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/RelationshipProxy.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipProxy.cs @@ -1,12 +1,10 @@ using System; using System.Collections; using System.Collections.Generic; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Hooks +namespace JsonApiDotNetCore.Hooks.Internal.Traversal { /// /// A class used internally for resource hook execution. Not intended for developer use. @@ -97,16 +95,16 @@ public void SetValue(IIdentifiable resource, object value, IResourceFactory reso var throughResources = (IEnumerable)hasManyThrough.ThroughProperty.GetValue(resource); var filteredList = new List(); - var rightResources = ((IEnumerable)value).CopyToList(RightType); + var rightResources = TypeHelper.CopyToList((IEnumerable)value, RightType); foreach (var throughResource in throughResources) { - if (((IList)rightResources).Contains(hasManyThrough.RightProperty.GetValue(throughResource))) + if (rightResources.Contains(hasManyThrough.RightProperty.GetValue(throughResource))) { filteredList.Add(throughResource); } } - var collectionValue = filteredList.CopyToTypedCollection(hasManyThrough.ThroughProperty.PropertyType); + var collectionValue = TypeHelper.CopyToTypedCollection(filteredList, hasManyThrough.ThroughProperty.PropertyType); hasManyThrough.ThroughProperty.SetValue(resource, collectionValue); return; } diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/RelationshipsFromPreviousLayer.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipsFromPreviousLayer.cs similarity index 57% rename from src/JsonApiDotNetCore/Hooks/Traversal/RelationshipsFromPreviousLayer.cs rename to src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipsFromPreviousLayer.cs index cb8aa7c63b..d5bd5e416a 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/RelationshipsFromPreviousLayer.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipsFromPreviousLayer.cs @@ -1,28 +1,11 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Hooks +namespace JsonApiDotNetCore.Hooks.Internal.Traversal { - /// - /// A helper class for mapping relationships between a current and previous layer - /// - internal interface IRelationshipsFromPreviousLayer - { - /// - /// Grouped by relationship to the previous layer, gets all the resources of the current layer - /// - /// The right side resources. - Dictionary GetRightResources(); - /// - /// Grouped by relationship to the previous layer, gets all the resources of the previous layer - /// - /// The right side resources. - Dictionary GetLeftResources(); - } - internal sealed class RelationshipsFromPreviousLayer : IRelationshipsFromPreviousLayer, IEnumerable> where TRightResource : class, IIdentifiable { private readonly IEnumerable> _collection; @@ -32,13 +15,13 @@ public RelationshipsFromPreviousLayer(IEnumerable + /// public Dictionary GetRightResources() { return _collection.ToDictionary(rg => rg.Proxy.Attribute, rg => (IEnumerable)rg.RightResources); } - /// + /// public Dictionary GetLeftResources() { return _collection.ToDictionary(rg => rg.Proxy.Attribute, rg => (IEnumerable)rg.LeftResources); diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/RootNode.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RootNode.cs similarity index 95% rename from src/JsonApiDotNetCore/Hooks/Traversal/RootNode.cs rename to src/JsonApiDotNetCore/Hooks/Internal/Traversal/RootNode.cs index 45d565c653..3c3103fd52 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/RootNode.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RootNode.cs @@ -2,11 +2,10 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Hooks +namespace JsonApiDotNetCore.Hooks.Internal.Traversal { /// /// The root node class of the breadth-first-traversal of resource data structures diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/TraversalHelper.cs similarity index 91% rename from src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs rename to src/JsonApiDotNetCore/Hooks/Internal/Traversal/TraversalHelper.cs index 014705604b..bf6a1234c5 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/TraversalHelper.cs @@ -3,16 +3,13 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; using RightType = System.Type; using LeftType = System.Type; -namespace JsonApiDotNetCore.Hooks +namespace JsonApiDotNetCore.Hooks.Internal.Traversal { /// /// A helper class used by the to traverse through @@ -161,7 +158,7 @@ private Dictionary UniqueInTree(IEnumerable resour /// Relationship attribute private RightType GetRightTypeFromRelationship(RelationshipAttribute attr) { - if (attr is HasManyThroughAttribute throughAttr && throughAttr.ThroughType.IsOrImplementsInterface(typeof(IIdentifiable))) + if (attr is HasManyThroughAttribute throughAttr && TypeHelper.IsOrImplementsInterface(throughAttr.ThroughType, typeof(IIdentifiable))) { return throughAttr.ThroughType; } @@ -293,7 +290,7 @@ private IResourceNode CreateNodeInstance(RightType nodeType, RelationshipProxy[] /// private IRelationshipsFromPreviousLayer CreateRelationshipsFromInstance(RightType nodeType, IEnumerable relationshipsFromPrev) { - var cast = relationshipsFromPrev.CopyToList(relationshipsFromPrev.First().GetType()); + var cast = TypeHelper.CopyToList(relationshipsFromPrev, relationshipsFromPrev.First().GetType()); return (IRelationshipsFromPreviousLayer)TypeHelper.CreateInstanceOfOpenType(typeof(RelationshipsFromPreviousLayer<>), nodeType, cast); } @@ -302,39 +299,9 @@ private IRelationshipsFromPreviousLayer CreateRelationshipsFromInstance(RightTyp /// private IRelationshipGroup CreateRelationshipGroupInstance(Type thisLayerType, RelationshipProxy proxy, List leftResources, List rightResources) { - var rightResourcesHashed = TypeHelper.CreateInstanceOfOpenType(typeof(HashSet<>), thisLayerType, rightResources.CopyToList(thisLayerType)); + var rightResourcesHashed = TypeHelper.CreateInstanceOfOpenType(typeof(HashSet<>), thisLayerType, TypeHelper.CopyToList(rightResources, thisLayerType)); return (IRelationshipGroup)TypeHelper.CreateInstanceOfOpenType(typeof(RelationshipGroup<>), thisLayerType, proxy, new HashSet(leftResources), rightResourcesHashed); } } - - /// - /// A helper class that represents all resources in the current layer that - /// are being traversed for which hooks will be executed (see IResourceHookExecutor) - /// - internal sealed class NodeLayer : IEnumerable - { - private readonly List _collection; - - public bool AnyResources() - { - return _collection.Any(n => n.UniqueResources.Cast().Any()); - } - - public NodeLayer(List nodes) - { - _collection = nodes; - } - - public IEnumerator GetEnumerator() - { - return _collection.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } } - diff --git a/src/JsonApiDotNetCore/Internal/Contracts/IResourceGraph.cs b/src/JsonApiDotNetCore/Internal/Contracts/IResourceGraph.cs deleted file mode 100644 index 8fb6e9f318..0000000000 --- a/src/JsonApiDotNetCore/Internal/Contracts/IResourceGraph.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; - -namespace JsonApiDotNetCore.Internal.Contracts -{ - /// - /// Responsible for retrieving the exposed resource fields (attributes and - /// relationships) of registered resources in the resource resourceGraph. - /// - public interface IResourceGraph : IResourceContextProvider - { - /// - /// Gets all fields (attributes and relationships) for - /// that are targeted by the selector. If no selector is provided, all - /// exposed fields are returned. - /// - /// The resource for which to retrieve fields - /// Should be of the form: (TResource e) => new { e.Field1, e.Field2 } - List GetFields(Expression> selector = null) where TResource : IIdentifiable; - /// - /// Gets all attributes for - /// that are targeted by the selector. If no selector is provided, all - /// exposed fields are returned. - /// - /// The resource for which to retrieve attributes - /// Should be of the form: (TResource e) => new { e.Attribute1, e.Attribute2 } - List GetAttributes(Expression> selector = null) where TResource : IIdentifiable; - /// - /// Gets all relationships for - /// that are targeted by the selector. If no selector is provided, all - /// exposed fields are returned. - /// - /// The resource for which to retrieve relationships - /// Should be of the form: (TResource e) => new { e.Relationship1, e.Relationship2 } - List GetRelationships(Expression> selector = null) where TResource : IIdentifiable; - /// - /// Gets all exposed fields (attributes and relationships) for type - /// - /// The resource type. Must extend IIdentifiable. - List GetFields(Type type); - /// - /// Gets all exposed attributes for type - /// - /// The resource type. Must extend IIdentifiable. - List GetAttributes(Type type); - /// - /// Gets all exposed relationships for type - /// - /// The resource type. Must extend IIdentifiable. - List GetRelationships(Type type); - /// - /// Traverses the resource resourceGraph for the inverse relationship of the provided - /// ; - /// - /// - RelationshipAttribute GetInverse(RelationshipAttribute relationship); - } -} diff --git a/src/JsonApiDotNetCore/Internal/Exceptions/JsonApiSetupException.cs b/src/JsonApiDotNetCore/Internal/Exceptions/JsonApiSetupException.cs deleted file mode 100644 index 5ab6400b65..0000000000 --- a/src/JsonApiDotNetCore/Internal/Exceptions/JsonApiSetupException.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace JsonApiDotNetCore.Internal -{ - public sealed class JsonApiSetupException : Exception - { - public JsonApiSetupException(string message) - : base(message) { } - - public JsonApiSetupException(string message, Exception innerException) - : base(message, innerException) { } - } -} diff --git a/src/JsonApiDotNetCore/Internal/Generics/GenericServiceFactory.cs b/src/JsonApiDotNetCore/Internal/Generics/GenericServiceFactory.cs deleted file mode 100644 index dd51b55532..0000000000 --- a/src/JsonApiDotNetCore/Internal/Generics/GenericServiceFactory.cs +++ /dev/null @@ -1,56 +0,0 @@ -using JsonApiDotNetCore.Services; -using System; - -namespace JsonApiDotNetCore.Internal.Generics -{ - /// - /// Used to generate a generic operations processor when the types - /// are not known until runtime. The typical use case would be for - /// accessing relationship data or resolving operations processors. - /// - public interface IGenericServiceFactory - { - /// - /// Constructs the generic type and locates the service, then casts to TInterface - /// - /// - /// (typeof(GenericProcessor<>), typeof(TResource)); - /// ]]> - /// - TInterface Get(Type openGenericType, Type resourceType); - - /// - /// Constructs the generic type and locates the service, then casts to TInterface - /// - /// - /// (typeof(GenericProcessor<>), typeof(TResource), typeof(TId)); - /// ]]> - /// - TInterface Get(Type openGenericType, Type resourceType, Type keyType); - } - - public sealed class GenericServiceFactory : IGenericServiceFactory - { - private readonly IServiceProvider _serviceProvider; - - public GenericServiceFactory(IScopedServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - public TInterface Get(Type openGenericType, Type resourceType) - => GetInternal(openGenericType, resourceType); - - public TInterface Get(Type openGenericType, Type resourceType, Type keyType) - => GetInternal(openGenericType, resourceType, keyType); - - private TInterface GetInternal(Type openGenericType, params Type[] types) - { - var concreteType = openGenericType.MakeGenericType(types); - - return (TInterface)_serviceProvider.GetService(concreteType); - } - } -} diff --git a/src/JsonApiDotNetCore/Internal/IdentifiableComparer.cs b/src/JsonApiDotNetCore/Internal/IdentifiableComparer.cs deleted file mode 100644 index fbec8f67cb..0000000000 --- a/src/JsonApiDotNetCore/Internal/IdentifiableComparer.cs +++ /dev/null @@ -1,22 +0,0 @@ -using JsonApiDotNetCore.Models; -using System.Collections.Generic; - -namespace JsonApiDotNetCore.Internal -{ - /// - /// Compares `IIdentifiable` with each other based on ID - /// - public sealed class IdentifiableComparer : IEqualityComparer - { - internal static readonly IdentifiableComparer Instance = new IdentifiableComparer(); - - public bool Equals(IIdentifiable x, IIdentifiable y) - { - return x.StringId == y.StringId; - } - public int GetHashCode(IIdentifiable obj) - { - return obj.StringId.GetHashCode(); - } - } -} diff --git a/src/JsonApiDotNetCore/Internal/ResourceContext.cs b/src/JsonApiDotNetCore/Internal/ResourceContext.cs deleted file mode 100644 index a8e2a1fab1..0000000000 --- a/src/JsonApiDotNetCore/Internal/ResourceContext.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Models.JsonApiDocuments; - -namespace JsonApiDotNetCore.Internal -{ - public class ResourceContext - { - /// - /// The exposed resource name - /// - public string ResourceName { get; set; } - - /// - /// The data model type - /// - public Type ResourceType { get; set; } - - /// - /// The identity member type - /// - public Type IdentityType { get; set; } - - /// - /// The concrete type. - /// We store this so that we don't need to re-compute the generic type. - /// - public Type ResourceDefinitionType { get; set; } - - /// - /// Exposed resource attributes. - /// See https://jsonapi.org/format/#document-resource-object-attributes. - /// - public List Attributes { get; set; } - - /// - /// Exposed resource relationships. - /// See https://jsonapi.org/format/#document-resource-object-relationships - /// - public List Relationships { get; set; } - - /// - /// Related entities that are not exposed as resource relationships. - /// - public List EagerLoads { get; set; } - - private List _fields; - public List Fields { get { return _fields ??= Attributes.Cast().Concat(Relationships).ToList(); } } - - /// - /// Configures which links to show in the - /// object for this resource. If set to , - /// the configuration will be read from . - /// Defaults to . - /// - public Links TopLevelLinks { get; internal set; } = Links.NotConfigured; - - /// - /// Configures which links to show in the - /// object for this resource. If set to , - /// the configuration will be read from . - /// Defaults to . - /// - public Links ResourceLinks { get; internal set; } = Links.NotConfigured; - - /// - /// Configures which links to show in the - /// for all relationships of the resource for which this attribute was instantiated. - /// If set to , the configuration will - /// be read from or - /// . Defaults to . - /// - public Links RelationshipLinks { get; internal set; } = Links.NotConfigured; - - public override string ToString() - { - return ResourceName; - } - } -} diff --git a/src/JsonApiDotNetCore/Internal/ValidationResults.cs b/src/JsonApiDotNetCore/Internal/ValidationResults.cs deleted file mode 100644 index d13b5c65da..0000000000 --- a/src/JsonApiDotNetCore/Internal/ValidationResults.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCore.Internal -{ - public sealed class ValidationResult - { - public ValidationResult(LogLevel logLevel, string message) - { - LogLevel = logLevel; - Message = message; - } - - public LogLevel LogLevel { get; } - public string Message { get; } - } -} diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index e2808125b5..092743c5cb 100644 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -28,17 +28,4 @@ - - - - true - true - bin\Release\netstandard2.0\JsonApiDotNetCore.xml - - - - - diff --git a/src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs b/src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs index 328ed90491..4fbb3a644c 100644 --- a/src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs @@ -1,10 +1,14 @@ -using JsonApiDotNetCore.Extensions; +using System; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Infrastructure; namespace JsonApiDotNetCore.Middleware { + /// + /// Transforms s without parameters for correct internal handling. + /// For example: return NotFound() -> return NotFound(null) + /// public sealed class ConvertEmptyActionResultFilter : IAlwaysRunResultFilter { public void OnResultExecuted(ResultExecutedContext context) @@ -13,6 +17,8 @@ public void OnResultExecuted(ResultExecutedContext context) public void OnResultExecuting(ResultExecutingContext context) { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (!context.HttpContext.IsJsonApiRequest()) { return; diff --git a/src/JsonApiDotNetCore/RequestServices/Contracts/EndpointKind.cs b/src/JsonApiDotNetCore/Middleware/EndpointKind.cs similarity index 90% rename from src/JsonApiDotNetCore/RequestServices/Contracts/EndpointKind.cs rename to src/JsonApiDotNetCore/Middleware/EndpointKind.cs index e116ab368e..ea5b9339c9 100644 --- a/src/JsonApiDotNetCore/RequestServices/Contracts/EndpointKind.cs +++ b/src/JsonApiDotNetCore/Middleware/EndpointKind.cs @@ -1,4 +1,4 @@ -namespace JsonApiDotNetCore.RequestServices.Contracts +namespace JsonApiDotNetCore.Middleware { public enum EndpointKind { diff --git a/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs b/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs index 3c5407d475..c0f3e2f6e0 100644 --- a/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs +++ b/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs @@ -2,12 +2,13 @@ using System.Diagnostics; using System.Net; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Middleware { + /// public class ExceptionHandler : IExceptionHandler { private readonly IJsonApiOptions _options; @@ -15,12 +16,16 @@ public class ExceptionHandler : IExceptionHandler public ExceptionHandler(ILoggerFactory loggerFactory, IJsonApiOptions options) { - _options = options; + if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); + + _options = options ?? throw new ArgumentNullException(nameof(options)); _logger = loggerFactory.CreateLogger(); } public ErrorDocument HandleException(Exception exception) { + if (exception == null) throw new ArgumentNullException(nameof(exception)); + Exception demystified = exception.Demystify(); LogException(demystified); @@ -38,6 +43,8 @@ private void LogException(Exception exception) protected virtual LogLevel GetLogLevel(Exception exception) { + if (exception == null) throw new ArgumentNullException(nameof(exception)); + if (exception is JsonApiException || exception is InvalidModelStateException) { return LogLevel.Information; @@ -48,6 +55,8 @@ protected virtual LogLevel GetLogLevel(Exception exception) protected virtual string GetLogMessage(Exception exception) { + if (exception == null) throw new ArgumentNullException(nameof(exception)); + return exception is JsonApiException jsonApiException ? jsonApiException.Error.Title : exception.Message; @@ -55,6 +64,8 @@ protected virtual string GetLogMessage(Exception exception) protected virtual ErrorDocument CreateErrorDocument(Exception exception) { + if (exception == null) throw new ArgumentNullException(nameof(exception)); + if (exception is InvalidModelStateException modelStateException) { return new ErrorDocument(modelStateException.Errors); diff --git a/src/JsonApiDotNetCore/Middleware/HeaderConstants.cs b/src/JsonApiDotNetCore/Middleware/HeaderConstants.cs index 7868140198..910cb1c17e 100644 --- a/src/JsonApiDotNetCore/Middleware/HeaderConstants.cs +++ b/src/JsonApiDotNetCore/Middleware/HeaderConstants.cs @@ -1,4 +1,4 @@ -namespace JsonApiDotNetCore +namespace JsonApiDotNetCore.Middleware { public static class HeaderConstants { diff --git a/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs b/src/JsonApiDotNetCore/Middleware/HttpContextExtensions.cs similarity index 50% rename from src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs rename to src/JsonApiDotNetCore/Middleware/HttpContextExtensions.cs index 07bd3b66f4..a2647de0d5 100644 --- a/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs +++ b/src/JsonApiDotNetCore/Middleware/HttpContextExtensions.cs @@ -1,28 +1,44 @@ +using System; using Microsoft.AspNetCore.Http; -namespace JsonApiDotNetCore.Extensions +namespace JsonApiDotNetCore.Middleware { public static class HttpContextExtensions { + /// + /// Indicates whether the currently executing HTTP request is being handled by JsonApiDotNetCore. + /// public static bool IsJsonApiRequest(this HttpContext httpContext) { + if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); + string value = httpContext.Items["IsJsonApiRequest"] as string; return value == bool.TrueString; } - internal static void SetJsonApiRequest(this HttpContext httpContext) + internal static void RegisterJsonApiRequest(this HttpContext httpContext) { + if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); + httpContext.Items["IsJsonApiRequest"] = bool.TrueString; } internal static void DisableValidator(this HttpContext httpContext, string propertyName, string model) { + if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); + if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); + if (model == null) throw new ArgumentNullException(nameof(model)); + var itemKey = $"JsonApiDotNetCore_DisableValidation_{model}_{propertyName}"; httpContext.Items[itemKey] = true; } internal static bool IsValidatorDisabled(this HttpContext httpContext, string propertyName, string model) { + if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); + if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); + if (model == null) throw new ArgumentNullException(nameof(model)); + return httpContext.Items.ContainsKey($"JsonApiDotNetCore_DisableValidation_{model}_{propertyName}") || httpContext.Items.ContainsKey($"JsonApiDotNetCore_DisableValidation_{model}_Relation"); } diff --git a/src/JsonApiDotNetCore/Internal/IControllerResourceMapping.cs b/src/JsonApiDotNetCore/Middleware/IControllerResourceMapping.cs similarity index 52% rename from src/JsonApiDotNetCore/Internal/IControllerResourceMapping.cs rename to src/JsonApiDotNetCore/Middleware/IControllerResourceMapping.cs index 47a078c94c..de48544e79 100644 --- a/src/JsonApiDotNetCore/Internal/IControllerResourceMapping.cs +++ b/src/JsonApiDotNetCore/Middleware/IControllerResourceMapping.cs @@ -1,14 +1,14 @@ using System; -namespace JsonApiDotNetCore.Internal +namespace JsonApiDotNetCore.Middleware { /// - /// Registry of which resource is associated with which controller. + /// Registry of which resource type is associated with which controller. /// public interface IControllerResourceMapping { /// - /// Get the associated resource with the controller with the provided controller name + /// Get the associated resource type for the provided controller name. /// Type GetAssociatedResource(string controllerName); } diff --git a/src/JsonApiDotNetCore/Middleware/IExceptionHandler.cs b/src/JsonApiDotNetCore/Middleware/IExceptionHandler.cs index 3b3a55d10c..2521794c08 100644 --- a/src/JsonApiDotNetCore/Middleware/IExceptionHandler.cs +++ b/src/JsonApiDotNetCore/Middleware/IExceptionHandler.cs @@ -1,5 +1,5 @@ using System; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Middleware { diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiExceptionFilterProvider.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiExceptionFilterProvider.cs index 53077d8315..3343c9084e 100644 --- a/src/JsonApiDotNetCore/Middleware/IJsonApiExceptionFilterProvider.cs +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiExceptionFilterProvider.cs @@ -1,20 +1,14 @@ -using System; +using System; namespace JsonApiDotNetCore.Middleware { /// /// Provides the type of the global exception filter that is configured in MVC during startup. /// This can be overridden to let JADNC use your own exception filter. The default exception filter used - /// is + /// is . /// public interface IJsonApiExceptionFilterProvider { Type Get(); } - - /// - public class JsonApiExceptionFilterProvider : IJsonApiExceptionFilterProvider - { - public Type Get() => typeof(JsonApiExceptionFilter); - } } diff --git a/src/JsonApiDotNetCore/RequestServices/Contracts/ICurrentRequest.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs similarity index 90% rename from src/JsonApiDotNetCore/RequestServices/Contracts/ICurrentRequest.cs rename to src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs index bd066f5ab2..b24729309f 100644 --- a/src/JsonApiDotNetCore/RequestServices/Contracts/ICurrentRequest.cs +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs @@ -1,13 +1,12 @@ using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.RequestServices.Contracts +namespace JsonApiDotNetCore.Middleware { /// - /// Metadata associated to the current json:api request. + /// Metadata associated with the json:api request that is currently being processed. /// - public interface ICurrentRequest + public interface IJsonApiRequest { /// /// Routing information, based on the path of the request URL. diff --git a/src/JsonApiDotNetCore/Internal/IJsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiRoutingConvention.cs similarity index 90% rename from src/JsonApiDotNetCore/Internal/IJsonApiRoutingConvention.cs rename to src/JsonApiDotNetCore/Middleware/IJsonApiRoutingConvention.cs index 7f616fbefa..3d4df4d29c 100644 --- a/src/JsonApiDotNetCore/Internal/IJsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiRoutingConvention.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels; -namespace JsonApiDotNetCore.Internal +namespace JsonApiDotNetCore.Middleware { /// /// Service for specifying which routing convention to use. This can be overridden to customize diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilterProvider.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilterProvider.cs index c3232c636b..e889ad65d1 100644 --- a/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilterProvider.cs +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilterProvider.cs @@ -5,16 +5,10 @@ namespace JsonApiDotNetCore.Middleware /// /// Provides the type of the global action filter that is configured in MVC during startup. /// This can be overridden to let JADNC use your own action filter. The default action filter used - /// is + /// is . /// public interface IJsonApiTypeMatchFilterProvider { Type Get(); } - - /// - public class JsonApiTypeMatchFilterProvider : IJsonApiTypeMatchFilterProvider - { - public Type Get() => typeof(IncomingTypeMatchFilter); - } } diff --git a/src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs b/src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs index f534bfddac..d591165542 100644 --- a/src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs @@ -1,10 +1,13 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Filters; namespace JsonApiDotNetCore.Middleware { + /// + /// Extensibility point for processing request query strings. + /// public interface IQueryStringActionFilter { Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next); } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Middleware/IncomingTypeMatchFilter.cs b/src/JsonApiDotNetCore/Middleware/IncomingTypeMatchFilter.cs index e46805cecc..8ee36e7e6c 100644 --- a/src/JsonApiDotNetCore/Middleware/IncomingTypeMatchFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/IncomingTypeMatchFilter.cs @@ -1,15 +1,14 @@ +using System; using System.Linq; using System.Net.Http; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; using Microsoft.AspNetCore.Mvc.Filters; namespace JsonApiDotNetCore.Middleware { /// - /// Action filter used to verify the incoming type matches the target type, else return a 409 + /// Action filter used to verify the incoming resource type matches the target type, else return a 409. /// public sealed class IncomingTypeMatchFilter : IActionFilter { @@ -22,6 +21,8 @@ public IncomingTypeMatchFilter(IResourceContextProvider provider) public void OnActionExecuting(ActionExecutingContext context) { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (!context.HttpContext.IsJsonApiRequest()) { return; diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs index 75474a3f41..f389f2de69 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore.Extensions; +using System; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; @@ -7,17 +7,19 @@ namespace JsonApiDotNetCore.Middleware /// /// Global exception filter that wraps any thrown error with a JsonApiException. /// - public class JsonApiExceptionFilter : ActionFilterAttribute, IExceptionFilter + public sealed class JsonApiExceptionFilter : ActionFilterAttribute, IExceptionFilter { private readonly IExceptionHandler _exceptionHandler; public JsonApiExceptionFilter(IExceptionHandler exceptionHandler) { - _exceptionHandler = exceptionHandler; + _exceptionHandler = exceptionHandler ?? throw new ArgumentNullException(nameof(exceptionHandler)); } public void OnException(ExceptionContext context) { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (context.HttpContext.IsJsonApiRequest()) { var errorDocument = _exceptionHandler.HandleException(context.Exception); diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilterProvider.cs b/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilterProvider.cs new file mode 100644 index 0000000000..6289718f69 --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilterProvider.cs @@ -0,0 +1,10 @@ +using System; + +namespace JsonApiDotNetCore.Middleware +{ + /// + public sealed class JsonApiExceptionFilterProvider : IJsonApiExceptionFilterProvider + { + public Type Get() => typeof(JsonApiExceptionFilter); + } +} diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiInputFormatter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs similarity index 63% rename from src/JsonApiDotNetCore/Formatters/JsonApiInputFormatter.cs rename to src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs index 815f5a75d2..4db839cc19 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiInputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs @@ -1,23 +1,27 @@ using System; using System.Threading.Tasks; -using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Serialization; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.DependencyInjection; -namespace JsonApiDotNetCore.Formatters +namespace JsonApiDotNetCore.Middleware { + /// + /// Extensibility point for reading incoming HTTP request. + /// public sealed class JsonApiInputFormatter : IInputFormatter { public bool CanRead(InputFormatterContext context) { - if (context == null) - throw new ArgumentNullException(nameof(context)); + if (context == null) throw new ArgumentNullException(nameof(context)); return context.HttpContext.IsJsonApiRequest(); } public async Task ReadAsync(InputFormatterContext context) { + if (context == null) throw new ArgumentNullException(nameof(context)); + var reader = context.HttpContext.RequestServices.GetService(); return await reader.ReadAsync(context); } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index f161e9a78b..853386a7e8 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Linq; using System.Net; @@ -6,13 +7,9 @@ using System.Text; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Models.JsonApiDocuments; -using JsonApiDotNetCore.RequestServices; -using JsonApiDotNetCore.RequestServices.Contracts; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; @@ -22,7 +19,7 @@ namespace JsonApiDotNetCore.Middleware { /// - /// Intercepts HTTP requests to populate injected instance for json:api requests. + /// Intercepts HTTP requests to populate injected instance for json:api requests. /// public sealed class JsonApiMiddleware { @@ -36,9 +33,15 @@ public JsonApiMiddleware(RequestDelegate next) public async Task Invoke(HttpContext httpContext, IControllerResourceMapping controllerResourceMapping, IJsonApiOptions options, - ICurrentRequest currentRequest, + IJsonApiRequest request, IResourceContextProvider resourceContextProvider) { + if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); + if (controllerResourceMapping == null) throw new ArgumentNullException(nameof(controllerResourceMapping)); + if (options == null) throw new ArgumentNullException(nameof(options)); + if (request == null) throw new ArgumentNullException(nameof(request)); + if (resourceContextProvider == null) throw new ArgumentNullException(nameof(resourceContextProvider)); + var routeValues = httpContext.GetRouteData().Values; var primaryResourceContext = CreatePrimaryResourceContext(routeValues, controllerResourceMapping, resourceContextProvider); @@ -50,9 +53,9 @@ public async Task Invoke(HttpContext httpContext, return; } - SetupCurrentRequest((CurrentRequest)currentRequest, primaryResourceContext, routeValues, options, resourceContextProvider, httpContext.Request); + SetupRequest((JsonApiRequest)request, primaryResourceContext, routeValues, options, resourceContextProvider, httpContext.Request); - httpContext.SetJsonApiRequest(); + httpContext.RegisterJsonApiRequest(); } await _next(httpContext); @@ -62,13 +65,16 @@ private static ResourceContext CreatePrimaryResourceContext(RouteValueDictionary IControllerResourceMapping controllerResourceMapping, IResourceContextProvider resourceContextProvider) { var controllerName = (string) routeValues["controller"]; - if (controllerName == null) + if (controllerName != null) { - return null; + var resourceType = controllerResourceMapping.GetAssociatedResource(controllerName); + if (resourceType != null) + { + return resourceContextProvider.GetResourceContext(resourceType); + } } - var resourceType = controllerResourceMapping.GetAssociatedResource(controllerName); - return resourceContextProvider.GetResourceContext(resourceType); + return null; } private static async Task ValidateContentTypeHeaderAsync(HttpContext httpContext, JsonSerializerSettings serializerSettings) @@ -151,20 +157,20 @@ private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSeri await httpResponse.Body.FlushAsync(); } - private static void SetupCurrentRequest(CurrentRequest currentRequest, ResourceContext primaryResourceContext, + private static void SetupRequest(JsonApiRequest request, ResourceContext primaryResourceContext, RouteValueDictionary routeValues, IJsonApiOptions options, IResourceContextProvider resourceContextProvider, HttpRequest httpRequest) { - currentRequest.IsReadOnly = httpRequest.Method == HttpMethod.Get.Method; - currentRequest.Kind = EndpointKind.Primary; - currentRequest.PrimaryResource = primaryResourceContext; - currentRequest.PrimaryId = GetPrimaryRequestId(routeValues); - currentRequest.BasePath = GetBasePath(primaryResourceContext.ResourceName, options, httpRequest); + request.IsReadOnly = httpRequest.Method == HttpMethod.Get.Method; + request.Kind = EndpointKind.Primary; + request.PrimaryResource = primaryResourceContext; + request.PrimaryId = GetPrimaryRequestId(routeValues); + request.BasePath = GetBasePath(primaryResourceContext.ResourceName, options, httpRequest); var relationshipName = GetRelationshipNameForSecondaryRequest(routeValues); if (relationshipName != null) { - currentRequest.Kind = IsRouteForRelationship(routeValues) ? EndpointKind.Relationship : EndpointKind.Secondary; + request.Kind = IsRouteForRelationship(routeValues) ? EndpointKind.Relationship : EndpointKind.Secondary; var requestRelationship = primaryResourceContext.Relationships.SingleOrDefault(relationship => @@ -172,12 +178,12 @@ private static void SetupCurrentRequest(CurrentRequest currentRequest, ResourceC if (requestRelationship != null) { - currentRequest.Relationship = requestRelationship; - currentRequest.SecondaryResource = resourceContextProvider.GetResourceContext(requestRelationship.RightType); + request.Relationship = requestRelationship; + request.SecondaryResource = resourceContextProvider.GetResourceContext(requestRelationship.RightType); } } - currentRequest.IsCollection = currentRequest.PrimaryId == null || currentRequest.Relationship is HasManyAttribute; + request.IsCollection = request.PrimaryId == null || request.Relationship is HasManyAttribute; } private static string GetPrimaryRequestId(RouteValueDictionary routeValues) @@ -235,7 +241,7 @@ private static string GetRelationshipNameForSecondaryRequest(RouteValueDictionar private static bool IsRouteForRelationship(RouteValueDictionary routeValues) { var actionName = (string)routeValues["action"]; - return actionName.EndsWith("Relationship"); + return actionName.EndsWith("Relationship", StringComparison.Ordinal); } } } diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs similarity index 63% rename from src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs rename to src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs index def858fd3a..89ab8da53b 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs @@ -1,23 +1,27 @@ using System; using System.Threading.Tasks; -using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Serialization; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.DependencyInjection; -namespace JsonApiDotNetCore.Formatters +namespace JsonApiDotNetCore.Middleware { + /// + /// Extensibility point for writing outgoing HTTP response. + /// public sealed class JsonApiOutputFormatter : IOutputFormatter { public bool CanWriteResult(OutputFormatterCanWriteContext context) { - if (context == null) - throw new ArgumentNullException(nameof(context)); + if (context == null) throw new ArgumentNullException(nameof(context)); return context.HttpContext.IsJsonApiRequest(); } public async Task WriteAsync(OutputFormatterWriteContext context) { + if (context == null) throw new ArgumentNullException(nameof(context)); + var writer = context.HttpContext.RequestServices.GetService(); await writer.WriteAsync(context); } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs new file mode 100644 index 0000000000..5080c5f9e2 --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs @@ -0,0 +1,33 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCore.Middleware +{ + /// + public sealed class JsonApiRequest : IJsonApiRequest + { + /// + public EndpointKind Kind { get; set; } + + /// + public string BasePath { get; set; } + + /// + public string PrimaryId { get; set; } + + /// + public ResourceContext PrimaryResource { get; set; } + + /// + public ResourceContext SecondaryResource { get; set; } + + /// + public RelationshipAttribute Relationship { get; set; } + + /// + public bool IsCollection { get; set; } + + /// + public bool IsReadOnly { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Internal/JsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs similarity index 87% rename from src/JsonApiDotNetCore/Internal/JsonApiRoutingConvention.cs rename to src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs index 72067044d2..0264c15797 100644 --- a/src/JsonApiDotNetCore/Internal/JsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs @@ -4,13 +4,13 @@ using System.Reflection; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Graph; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Resources; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; -namespace JsonApiDotNetCore.Internal +namespace JsonApiDotNetCore.Middleware { /// /// The default routing convention registers the name of the resource as the route @@ -23,7 +23,7 @@ namespace JsonApiDotNetCore.Internal /// /// public class RandomNameController : JsonApiController { } // => /someResources/relationship/relatedResource /// - /// // when using the kebab-case formatter: + /// // when using kebab-case casing convention: /// public class SomeResourceController : JsonApiController { } // => /some-resources/relationship/related-resource /// /// public class SomeVeryCustomController : CoreJsonApiController { } // => /someVeryCustoms/relationship/relatedResource @@ -37,20 +37,24 @@ public class JsonApiRoutingConvention : IJsonApiRoutingConvention public JsonApiRoutingConvention(IJsonApiOptions options) { - _options = options; + _options = options ?? throw new ArgumentNullException(nameof(options)); _formatter = new ResourceNameFormatter(options); } - /// + /// public Type GetAssociatedResource(string controllerName) { + if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); + _registeredResources.TryGetValue(controllerName, out Type type); return type; } - /// + /// public void Apply(ApplicationModel application) { + if (application == null) throw new ArgumentNullException(nameof(application)); + foreach (var controller in application.Controllers) { var resourceType = GetResourceTypeFromController(controller.ControllerType); @@ -58,12 +62,12 @@ public void Apply(ApplicationModel application) if (resourceType != null) _registeredResources.Add(controller.ControllerName, resourceType); - if (RoutingConventionDisabled(controller) == false) + if (!RoutingConventionDisabled(controller)) continue; var template = TemplateFromResource(controller) ?? TemplateFromController(controller); if (template == null) - throw new JsonApiSetupException($"Controllers with overlapping route templates detected: {controller.ControllerType.FullName}"); + throw new InvalidConfigurationException($"Controllers with overlapping route templates detected: {controller.ControllerType.FullName}"); controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel { Template = template }; } @@ -128,7 +132,7 @@ private Type GetResourceTypeFromController(Type type) if ((nextBaseType == aspNetControllerType || nextBaseType == coreControllerType) && currentType.IsGenericType) { - var resourceType = currentType.GetGenericArguments().FirstOrDefault(t => t.IsOrImplementsInterface(typeof(IIdentifiable))); + var resourceType = currentType.GetGenericArguments().FirstOrDefault(t => TypeHelper.IsOrImplementsInterface(t, typeof(IIdentifiable))); if (resourceType != null) { return resourceType; diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiTypeMatchFilterProvider.cs b/src/JsonApiDotNetCore/Middleware/JsonApiTypeMatchFilterProvider.cs new file mode 100644 index 0000000000..f6cbb7a743 --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/JsonApiTypeMatchFilterProvider.cs @@ -0,0 +1,10 @@ +using System; + +namespace JsonApiDotNetCore.Middleware +{ + /// + public class JsonApiTypeMatchFilterProvider : IJsonApiTypeMatchFilterProvider + { + public Type Get() => typeof(IncomingTypeMatchFilter); + } +} diff --git a/src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs b/src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs index 2122e8d220..5ed52517f3 100644 --- a/src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs @@ -1,6 +1,7 @@ +using System; using System.Reflection; using System.Threading.Tasks; -using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Controllers.Annotations; using JsonApiDotNetCore.QueryStrings; using Microsoft.AspNetCore.Mvc.Filters; @@ -12,14 +13,17 @@ public sealed class QueryStringActionFilter : IAsyncActionFilter, IQueryStringAc public QueryStringActionFilter(IQueryStringReader queryStringReader) { - _queryStringReader = queryStringReader; + _queryStringReader = queryStringReader ?? throw new ArgumentNullException(nameof(queryStringReader)); } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { - DisableQueryAttribute disableQueryAttribute = context.Controller.GetType().GetCustomAttribute(); + if (context == null) throw new ArgumentNullException(nameof(context)); + if (next == null) throw new ArgumentNullException(nameof(next)); - _queryStringReader.ReadAll(disableQueryAttribute); + DisableQueryStringAttribute disableQueryStringAttribute = context.Controller.GetType().GetCustomAttribute(); + + _queryStringReader.ReadAll(disableQueryStringAttribute); await next(); } } diff --git a/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs b/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs new file mode 100644 index 0000000000..6e03c521dc --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs @@ -0,0 +1,143 @@ +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.Middleware +{ + internal sealed class TraceLogWriter + { + private readonly ILogger _logger; + + private bool IsEnabled => _logger.IsEnabled(LogLevel.Trace); + + public TraceLogWriter(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(typeof(T)); + } + + public void LogMethodStart(object parameters = null, [CallerMemberName] string memberName = "") + { + if (IsEnabled) + { + string message = FormatMessage(memberName, parameters); + WriteMessageToLog(message); + } + } + + public void LogMessage(Func messageFactory) + { + if (IsEnabled) + { + string message = messageFactory(); + WriteMessageToLog(message); + } + } + + private static string FormatMessage(string memberName, object parameters) + { + var builder = new StringBuilder(); + + builder.Append("Entering "); + builder.Append(memberName); + builder.Append("("); + WriteProperties(builder, parameters); + builder.Append(")"); + + return builder.ToString(); + } + + private static void WriteProperties(StringBuilder builder, object propertyContainer) + { + if (propertyContainer != null) + { + bool isFirstMember = true; + foreach (var property in propertyContainer.GetType().GetProperties()) + { + if (isFirstMember) + { + isFirstMember = false; + } + else + { + builder.Append(", "); + } + + WriteProperty(builder, property, propertyContainer); + } + } + } + + private static void WriteProperty(StringBuilder builder, PropertyInfo property, object instance) + { + builder.Append(property.Name); + builder.Append(": "); + + var value = property.GetValue(instance); + if (value == null) + { + builder.Append("null"); + } + else if (value is string stringValue) + { + builder.Append("\""); + builder.Append(stringValue); + builder.Append("\""); + } + else + { + WriteObject(builder, value); + } + } + + private static void WriteObject(StringBuilder builder, object value) + { + if (HasToStringOverload(value.GetType())) + { + builder.Append(value); + } + else + { + var text = SerializeObject(value); + builder.Append(text); + } + } + + private static bool HasToStringOverload(Type type) + { + if (type != null) + { + var toStringMethod = type.GetMethod("ToString", Array.Empty()); + if (toStringMethod != null && toStringMethod.DeclaringType != typeof(object)) + { + return true; + } + } + + return false; + } + + private static string SerializeObject(object value) + { + try + { + // It turns out setting ReferenceLoopHandling to something other than Error only takes longer to fail. + // This is because Newtonsoft.Json always tries to serialize the first element in a graph. And with + // EF Core models, that one is often recursive, resulting in either StackOverflowException or OutOfMemoryException. + return JsonConvert.SerializeObject(value, Formatting.Indented); + } + catch (JsonSerializationException) + { + // Never crash as a result of logging, this is best-effort only. + return "object"; + } + } + + private void WriteMessageToLog(string message) + { + _logger.LogTrace(message); + } + } +} diff --git a/src/JsonApiDotNetCore/Models/Annotation/AttrAttribute.cs b/src/JsonApiDotNetCore/Models/Annotation/AttrAttribute.cs deleted file mode 100644 index 79871f9ce6..0000000000 --- a/src/JsonApiDotNetCore/Models/Annotation/AttrAttribute.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using JsonApiDotNetCore.Internal; - -namespace JsonApiDotNetCore.Models.Annotation -{ - [AttributeUsage(AttributeTargets.Property)] - public sealed class AttrAttribute : ResourceFieldAttribute - { - internal bool HasExplicitCapabilities { get; } - - public AttrCapabilities Capabilities { get; internal set; } - - /// - /// Exposes a resource property as a json:api attribute using the configured casing convention and capabilities. - /// - /// - /// - /// public class Author : Identifiable - /// { - /// [Attr] - /// public string Name { get; set; } - /// } - /// - /// - public AttrAttribute() - { - } - - /// - /// Exposes a resource property as a json:api attribute with an explicit name, using configured capabilities. - /// - public AttrAttribute(string publicName) - : base(publicName) - { - if (publicName == null) - { - throw new ArgumentNullException(nameof(publicName)); - } - } - - /// - /// Exposes a resource property as a json:api attribute using the configured casing convention and an explicit set of capabilities. - /// - /// - /// - /// public class Author : Identifiable - /// { - /// [Attr(AttrCapabilities.AllowFilter | AttrCapabilities.AllowSort)] - /// public string Name { get; set; } - /// } - /// - /// - public AttrAttribute(AttrCapabilities capabilities) - { - HasExplicitCapabilities = true; - Capabilities = capabilities; - } - - /// - /// Exposes a resource property as a json:api attribute with an explicit name and capabilities. - /// - public AttrAttribute(string publicName, AttrCapabilities capabilities) - : this(publicName) - { - HasExplicitCapabilities = true; - Capabilities = capabilities; - } - - /// - /// Get the value of the attribute for the given object. - /// Returns null if the attribute does not belong to the - /// provided object. - /// - public object GetValue(object resource) - { - if (resource == null) - { - throw new ArgumentNullException(nameof(resource)); - } - - if (Property.GetMethod == null) - { - throw new InvalidOperationException($"Property '{Property.DeclaringType?.Name}.{Property.Name}' is write-only."); - } - - return Property.GetValue(resource); - } - - /// - /// Sets the value of the attribute on the given object. - /// - public void SetValue(object resource, object newValue) - { - if (resource == null) - { - throw new ArgumentNullException(nameof(resource)); - } - - if (Property.SetMethod == null) - { - throw new InvalidOperationException( - $"Property '{Property.DeclaringType?.Name}.{Property.Name}' is read-only."); - } - - var convertedValue = TypeHelper.ConvertType(newValue, Property.PropertyType); - Property.SetValue(resource, convertedValue); - } - } -} diff --git a/src/JsonApiDotNetCore/Models/Annotation/HasManyAttribute.cs b/src/JsonApiDotNetCore/Models/Annotation/HasManyAttribute.cs deleted file mode 100644 index 8d6121a775..0000000000 --- a/src/JsonApiDotNetCore/Models/Annotation/HasManyAttribute.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using JsonApiDotNetCore.Models.JsonApiDocuments; - -namespace JsonApiDotNetCore.Models.Annotation -{ - [AttributeUsage(AttributeTargets.Property)] - public class HasManyAttribute : RelationshipAttribute - { - /// - /// Create a HasMany relational link to another resource - /// - /// - /// The relationship name as exposed by the API - /// Which links are available. Defaults to - /// Whether or not this relationship can be included using the ?include=public-name query string - /// - /// - /// Articles { get; set; } - /// } - /// ]]> - /// - public HasManyAttribute(string publicName = null, Links relationshipLinks = Links.All, bool canInclude = true, string inverseNavigationProperty = null) - : base(publicName, relationshipLinks, canInclude) - { - InverseNavigation = inverseNavigationProperty; - } - } -} diff --git a/src/JsonApiDotNetCore/Models/Annotation/HasManyThroughAttribute.cs b/src/JsonApiDotNetCore/Models/Annotation/HasManyThroughAttribute.cs deleted file mode 100644 index 29f18cc92f..0000000000 --- a/src/JsonApiDotNetCore/Models/Annotation/HasManyThroughAttribute.cs +++ /dev/null @@ -1,212 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models.JsonApiDocuments; - -namespace JsonApiDotNetCore.Models.Annotation -{ - /// - /// Create a HasMany relationship through a many-to-many join relationship. - /// This type can only be applied on types that implement ICollection. - /// - /// - /// - /// In the following example, we expose a relationship named "tags" - /// through the navigation property `ArticleTags`. - /// The `Tags` property is decorated with `NotMapped` so that EF does not try - /// to map this to a database relationship. - /// Tags { get; set; } - /// public ICollection ArticleTags { get; set; } - /// ]]> - /// - [AttributeUsage(AttributeTargets.Property)] - public sealed class HasManyThroughAttribute : HasManyAttribute - { - /// - /// The name of the join property on the parent resource. - /// - /// - /// In the `[HasManyThrough("tags", nameof(ArticleTags))]` example - /// this would be "ArticleTags". - /// - internal string ThroughPropertyName { get; } - - /// - /// The join type. - /// - /// - /// In the `[HasManyThrough("tags", nameof(ArticleTags))]` example - /// this would be `ArticleTag`. - /// - public Type ThroughType { get; internal set; } - - /// - /// The navigation property back to the parent resource from the through type. - /// - /// - /// - /// In the `[HasManyThrough("tags", nameof(ArticleTags))]` example - /// this would point to the `Article.ArticleTags.Article` property - /// - /// - /// public Article Article { get; set; } - /// - /// - /// - public PropertyInfo LeftProperty { get; internal set; } - - /// - /// The id property back to the parent resource from the through type. - /// - /// - /// - /// In the `[HasManyThrough("tags", nameof(ArticleTags))]` example - /// this would point to the `Article.ArticleTags.ArticleId` property - /// - /// - /// public int ArticleId { get; set; } - /// - /// - /// - public PropertyInfo LeftIdProperty { get; internal set; } - - /// - /// The navigation property to the related resource from the through type. - /// - /// - /// - /// In the `[HasManyThrough("tags", nameof(ArticleTags))]` example - /// this would point to the `Article.ArticleTags.Tag` property - /// - /// - /// public Tag Tag { get; set; } - /// - /// - /// - public PropertyInfo RightProperty { get; internal set; } - - /// - /// The id property to the related resource from the through type. - /// - /// - /// - /// In the `[HasManyThrough("tags", nameof(ArticleTags))]` example - /// this would point to the `Article.ArticleTags.TagId` property - /// - /// - /// public int TagId { get; set; } - /// - /// - /// - public PropertyInfo RightIdProperty { get; internal set; } - - /// - /// The join resource property on the parent resource. - /// - /// - /// - /// In the `[HasManyThrough("tags", nameof(ArticleTags))]` example - /// this would point to the `Article.ArticleTags` property - /// - /// ArticleTags { get; set; } - /// ]]> - /// - /// - public PropertyInfo ThroughProperty { get; internal set; } - - /// - /// - /// "ArticleTags.Tag" - /// - public override string RelationshipPath => $"{ThroughProperty.Name}.{RightProperty.Name}"; - - /// - /// Create a HasMany relationship through a many-to-many join relationship. - /// The public name exposed through the API will be based on the configured convention. - /// - /// - /// The name of the navigation property that will be used to get the HasMany relationship - /// Which links are available. Defaults to - /// Whether or not this relationship can be included using the ?include=public-name query string - /// - /// - /// - /// [HasManyThrough(nameof(ArticleTags), relationshipLinks: Links.All, canInclude: true)] - /// - /// - public HasManyThroughAttribute(string throughPropertyName, Links relationshipLinks = Links.All, bool canInclude = true) - : base(null, relationshipLinks, canInclude) - { - ThroughPropertyName = throughPropertyName; - } - - /// - /// Create a HasMany relationship through a many-to-many join relationship. - /// - /// - /// The relationship name as exposed by the API - /// The name of the navigation property that will be used to get the HasMany relationship - /// Which links are available. Defaults to - /// Whether or not this relationship can be included using the ?include=public-name query string - /// - /// - /// - /// [HasManyThrough("tags", nameof(ArticleTags), relationshipLinks: Links.All, canInclude: true)] - /// - /// - public HasManyThroughAttribute(string publicName, string throughPropertyName, Links relationshipLinks = Links.All, bool canInclude = true) - : base(publicName, relationshipLinks, canInclude) - { - ThroughPropertyName = throughPropertyName; - } - - /// - /// Traverses through the provided resource and returns the - /// value of the relationship on the other side of a through type - /// (e.g. Articles.ArticleTags.Tag). - /// - public override object GetValue(object resource) - { - IEnumerable throughResources = (IEnumerable)ThroughProperty.GetValue(resource) ?? Array.Empty(); - - IEnumerable rightResources = throughResources - .Cast() - .Select(rightResource => RightProperty.GetValue(rightResource)); - - return rightResources.CopyToTypedCollection(Property.PropertyType); - } - - /// - public override void SetValue(object resource, object newValue, IResourceFactory resourceFactory) - { - base.SetValue(resource, newValue, resourceFactory); - - if (newValue == null) - { - ThroughProperty.SetValue(resource, null); - } - else - { - List throughResources = new List(); - foreach (IIdentifiable identifiable in (IEnumerable)newValue) - { - object throughResource = resourceFactory.CreateInstance(ThroughType); - LeftProperty.SetValue(throughResource, resource); - RightProperty.SetValue(throughResource, identifiable); - throughResources.Add(throughResource); - } - - var typedCollection = throughResources.CopyToTypedCollection(ThroughProperty.PropertyType); - ThroughProperty.SetValue(resource, typedCollection); - } - } - } -} diff --git a/src/JsonApiDotNetCore/Models/Annotation/HasOneAttribute.cs b/src/JsonApiDotNetCore/Models/Annotation/HasOneAttribute.cs deleted file mode 100644 index c8dba73984..0000000000 --- a/src/JsonApiDotNetCore/Models/Annotation/HasOneAttribute.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models.JsonApiDocuments; - -namespace JsonApiDotNetCore.Models.Annotation -{ - [AttributeUsage(AttributeTargets.Property)] - public sealed class HasOneAttribute : RelationshipAttribute - { - private readonly string _explicitIdentifiablePropertyName; - - /// - /// The independent resource identifier. - /// - public string IdentifiablePropertyName => - string.IsNullOrWhiteSpace(_explicitIdentifiablePropertyName) - ? JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(Property.Name) - : _explicitIdentifiablePropertyName; - - /// - /// Create a HasOne relational link to another resource - /// - /// - /// The relationship name as exposed by the API - /// Enum to set which links should be outputted for this relationship. Defaults to which means that the configuration in - /// or is used. - /// Whether or not this relationship can be included using the ?include=public-name query string - /// The foreign key property name. Defaults to "{RelationshipName}Id" - /// - /// - /// Using an alternative foreign key: - /// - /// - /// public class Article : Identifiable - /// { - /// [HasOne("author", withForeignKey: nameof(AuthorKey)] - /// public Author Author { get; set; } - /// public int AuthorKey { get; set; } - /// } - /// - /// - public HasOneAttribute(string publicName = null, Links links = Links.NotConfigured, bool canInclude = true, string withForeignKey = null, string inverseNavigationProperty = null) - : base(publicName, links, canInclude) - { - _explicitIdentifiablePropertyName = withForeignKey; - InverseNavigation = inverseNavigationProperty; - } - - /// - public override void SetValue(object resource, object newValue, IResourceFactory resourceFactory) - { - // If we're deleting the relationship (setting it to null), we set the foreignKey to null. - // We could also set the actual property to null, but then we would first need to load the - // current relationship, which requires an extra query. - - var propertyName = newValue == null ? IdentifiablePropertyName : Property.Name; - var resourceType = resource.GetType(); - - var propertyInfo = resourceType.GetProperty(propertyName); - if (propertyInfo == null) - { - // we can't set the FK to null because there isn't any. - propertyInfo = resourceType.GetProperty(RelationshipPath); - } - - propertyInfo.SetValue(resource, newValue); - } - } -} diff --git a/src/JsonApiDotNetCore/Models/Annotation/LinksAttribute.cs b/src/JsonApiDotNetCore/Models/Annotation/LinksAttribute.cs deleted file mode 100644 index 3e90dd0bb8..0000000000 --- a/src/JsonApiDotNetCore/Models/Annotation/LinksAttribute.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models.JsonApiDocuments; - -namespace JsonApiDotNetCore.Models.Annotation -{ - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] - public sealed class LinksAttribute : Attribute - { - public LinksAttribute(Links topLevelLinks = Links.NotConfigured, Links resourceLinks = Links.NotConfigured, Links relationshipLinks = Links.NotConfigured) - { - if (topLevelLinks == Links.Related) - throw new JsonApiSetupException($"{Links.Related:g} not allowed for argument {nameof(topLevelLinks)}"); - - if (resourceLinks == Links.Paging) - throw new JsonApiSetupException($"{Links.Paging:g} not allowed for argument {nameof(resourceLinks)}"); - - if (relationshipLinks == Links.Paging) - throw new JsonApiSetupException($"{Links.Paging:g} not allowed for argument {nameof(relationshipLinks)}"); - - TopLevelLinks = topLevelLinks; - ResourceLinks = resourceLinks; - RelationshipLinks = relationshipLinks; - } - - /// - /// Configures which links to show in the - /// object for this resource. - /// - public Links TopLevelLinks { get; } - - /// - /// Configures which links to show in the - /// object for this resource. - /// - public Links ResourceLinks { get; } - - /// - /// Configures which links to show in the - /// for all relationships of the resource for which this attribute was instantiated. - /// - public Links RelationshipLinks { get; } - } -} diff --git a/src/JsonApiDotNetCore/Models/Annotation/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Models/Annotation/RelationshipAttribute.cs deleted file mode 100644 index df4d3e4918..0000000000 --- a/src/JsonApiDotNetCore/Models/Annotation/RelationshipAttribute.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models.JsonApiDocuments; - -namespace JsonApiDotNetCore.Models.Annotation -{ - public abstract class RelationshipAttribute : ResourceFieldAttribute - { - public string InverseNavigation { get; internal set; } - - /// - /// The internal navigation property path to the related resource. - /// - /// - /// In all cases except the HasManyThrough relationships, this will just be the property name. - /// - public virtual string RelationshipPath => Property.Name; - - /// - /// The related resource type. This does not necessarily match the navigation property type. - /// In the case of a HasMany relationship, this value will be the generic argument type. - /// - /// - /// Tags { get; set; } // Type => Tag - /// ]]> - /// - public Type RightType { get; internal set; } - - /// - /// The parent resource type. This is the type of the class in which this attribute was used. - /// - public Type LeftType { get; internal set; } - - /// - /// Configures which links to show in the - /// object for this relationship. - /// - public Links RelationshipLinks { get; } - - public bool CanInclude { get; } - - protected RelationshipAttribute(string publicName, Links relationshipLinks, bool canInclude) - : base(publicName) - { - if (relationshipLinks == Links.Paging) - throw new JsonApiSetupException($"{Links.Paging:g} not allowed for argument {nameof(relationshipLinks)}"); - - RelationshipLinks = relationshipLinks; - CanInclude = canInclude; - } - - /// - /// Gets the value of the resource property this attributes was declared on. - /// - public virtual object GetValue(object resource) - { - return Property.GetValue(resource); - } - - /// - /// Sets the value of the resource property this attributes was declared on. - /// - public virtual void SetValue(object resource, object newValue, IResourceFactory resourceFactory) - { - Property.SetValue(resource, newValue); - } - - public override bool Equals(object obj) - { - if (obj == null || GetType() != obj.GetType()) - { - return false; - } - - var other = (RelationshipAttribute) obj; - - return PublicName == other.PublicName && LeftType == other.LeftType && - RightType == other.RightType; - } - - public override int GetHashCode() - { - return HashCode.Combine(PublicName, LeftType, RightType); - } - } -} diff --git a/src/JsonApiDotNetCore/Models/Annotation/ResourceAttribute.cs b/src/JsonApiDotNetCore/Models/Annotation/ResourceAttribute.cs deleted file mode 100644 index 4178f27dad..0000000000 --- a/src/JsonApiDotNetCore/Models/Annotation/ResourceAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace JsonApiDotNetCore.Models.Annotation -{ - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] - public sealed class ResourceAttribute : Attribute - { - public ResourceAttribute(string resourceName) - { - ResourceName = resourceName; - } - - public string ResourceName { get; } - } -} diff --git a/src/JsonApiDotNetCore/Models/Annotation/ResourceFieldAttribute.cs b/src/JsonApiDotNetCore/Models/Annotation/ResourceFieldAttribute.cs deleted file mode 100644 index 2ce3975179..0000000000 --- a/src/JsonApiDotNetCore/Models/Annotation/ResourceFieldAttribute.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Reflection; - -namespace JsonApiDotNetCore.Models.Annotation -{ - /// - /// Used to expose a resource property as a json:api field (attribute or relationship). - /// - public abstract class ResourceFieldAttribute : Attribute - { - /// - /// The publicly exposed name of this json:api field. - /// - public string PublicName { get; internal set; } - - /// - /// The resource property that this attribute is declared on. - /// - public PropertyInfo Property { get; internal set; } - - protected ResourceFieldAttribute() - { - } - - protected ResourceFieldAttribute(string publicName) - { - if (publicName != null && string.IsNullOrWhiteSpace(publicName)) - { - throw new ArgumentException("Exposed name cannot be empty or contain only whitespace.", - nameof(publicName)); - } - - PublicName = publicName; - } - - public override string ToString() - { - return PublicName ?? (Property != null ? Property.Name : base.ToString()); - } - } -} diff --git a/src/JsonApiDotNetCore/Models/IHasMeta.cs b/src/JsonApiDotNetCore/Models/IHasMeta.cs deleted file mode 100644 index f1605bf790..0000000000 --- a/src/JsonApiDotNetCore/Models/IHasMeta.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace JsonApiDotNetCore.Models -{ - public interface IHasMeta - { - Dictionary GetMeta(); - } -} diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/IIdentifiable.cs b/src/JsonApiDotNetCore/Models/JsonApiDocuments/IIdentifiable.cs deleted file mode 100644 index 93d7f859da..0000000000 --- a/src/JsonApiDotNetCore/Models/JsonApiDocuments/IIdentifiable.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace JsonApiDotNetCore.Models -{ - public interface IIdentifiable - { - string StringId { get; set; } - } - - public interface IIdentifiable : IIdentifiable - { - T Id { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/Identifiable.cs b/src/JsonApiDotNetCore/Models/JsonApiDocuments/Identifiable.cs deleted file mode 100644 index f559cf9aa3..0000000000 --- a/src/JsonApiDotNetCore/Models/JsonApiDocuments/Identifiable.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations.Schema; -using JsonApiDotNetCore.Internal; - -namespace JsonApiDotNetCore.Models -{ - public abstract class Identifiable : Identifiable - { } - - public abstract class Identifiable : IIdentifiable - { - /// - /// The resource identifier - /// - public virtual T Id { get; set; } - - /// - /// The string representation of the `Id`. - /// - /// This is used in serialization and deserialization. - /// The getters should handle the conversion - /// from `typeof(T)` to a string and the setter vice versa. - /// - /// To override this behavior, you can either implement the - /// interface directly or override - /// `GetStringId` and `GetTypedId` methods. - /// - [NotMapped] - public string StringId - { - get => GetStringId(Id); - set => Id = GetTypedId(value); - } - - /// - /// Convert the provided resource identifier to a string. - /// - protected virtual string GetStringId(object value) - { - if(value == null) - return string.Empty; // todo; investigate why not using null, because null would make more sense in serialization - - var type = typeof(T); - var stringValue = value.ToString(); - - if (type == typeof(Guid)) - { - var guid = Guid.Parse(stringValue); - return guid == Guid.Empty ? string.Empty : stringValue; - } - - return stringValue == "0" - ? string.Empty - : stringValue; - } - - /// - /// Convert a string to a typed resource identifier. - /// - protected virtual T GetTypedId(string value) - { - if (value == null) - return default; - return (T)TypeHelper.ConvertType(value, typeof(T)); - } - } -} diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceObjectComparer.cs b/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceObjectComparer.cs deleted file mode 100644 index 442a918687..0000000000 --- a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceObjectComparer.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCore.Builders -{ - internal sealed class ResourceObjectComparer : IEqualityComparer - { - public bool Equals(ResourceObject x, ResourceObject y) - { - return x.Id.Equals(y.Id) && x.Type.Equals(y.Type); - } - - public int GetHashCode(ResourceObject ro) - { - return ro.GetHashCode(); - } - } -} diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/TopLevelLinks.cs b/src/JsonApiDotNetCore/Models/JsonApiDocuments/TopLevelLinks.cs deleted file mode 100644 index 9b0992526a..0000000000 --- a/src/JsonApiDotNetCore/Models/JsonApiDocuments/TopLevelLinks.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Models.JsonApiDocuments -{ - /// - /// see links section in https://jsonapi.org/format/#document-top-level - /// - public sealed class TopLevelLinks - { - [JsonProperty("self")] - public string Self { get; set; } - - [JsonProperty("next")] - public string Next { get; set; } - - [JsonProperty("prev")] - public string Prev { get; set; } - - [JsonProperty("first")] - public string First { get; set; } - - [JsonProperty("last")] - public string Last { get; set; } - - // http://www.newtonsoft.com/json/help/html/ConditionalProperties.htm - public bool ShouldSerializeSelf() - { - return (!string.IsNullOrEmpty(Self)); - } - - public bool ShouldSerializeFirst() - { - return (!string.IsNullOrEmpty(First)); - } - - public bool ShouldSerializeNext() - { - return (!string.IsNullOrEmpty(Next)); - } - - public bool ShouldSerializePrev() - { - return (!string.IsNullOrEmpty(Prev)); - } - - public bool ShouldSerializeLast() - { - return (!string.IsNullOrEmpty(Last)); - } - } -} diff --git a/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs b/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs index 7c21e6f218..aea73b3126 100644 --- a/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs +++ b/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs @@ -1,3 +1,6 @@ -using System.Runtime.CompilerServices; - +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("Benchmarks")] [assembly: InternalsVisibleTo("IntegrationTests")] +[assembly:InternalsVisibleTo("JsonApiDotNetCoreExampleTests")] +[assembly:InternalsVisibleTo("UnitTests")] diff --git a/src/JsonApiDotNetCore/Queries/ExpressionInScope.cs b/src/JsonApiDotNetCore/Queries/ExpressionInScope.cs index 32c55aebf4..b24257a3fc 100644 --- a/src/JsonApiDotNetCore/Queries/ExpressionInScope.cs +++ b/src/JsonApiDotNetCore/Queries/ExpressionInScope.cs @@ -1,5 +1,5 @@ using System; -using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; namespace JsonApiDotNetCore.Queries diff --git a/src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs index f8ce03258e..2642874112 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs @@ -1,5 +1,5 @@ using System; -using JsonApiDotNetCore.Internal.Queries.Parsing; +using JsonApiDotNetCore.Queries.Internal.Parsing; namespace JsonApiDotNetCore.Queries.Expressions { diff --git a/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs index a48b3146e9..80473bf3c7 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs @@ -1,5 +1,5 @@ using System; -using JsonApiDotNetCore.Internal.Queries.Parsing; +using JsonApiDotNetCore.Queries.Internal.Parsing; namespace JsonApiDotNetCore.Queries.Expressions { diff --git a/src/JsonApiDotNetCore/Queries/Expressions/EqualsAnyOfExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/EqualsAnyOfExpression.cs index 47e839742f..18671bb8b0 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/EqualsAnyOfExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/EqualsAnyOfExpression.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using JsonApiDotNetCore.Internal.Queries.Parsing; +using JsonApiDotNetCore.Queries.Internal.Parsing; namespace JsonApiDotNetCore.Queries.Expressions { diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs index 80eed65603..b46759f85f 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries.Expressions { @@ -39,7 +39,7 @@ public static IReadOnlyCollection GetRelationshipC IncludeToChainsConverter converter = new IncludeToChainsConverter(); converter.Visit(include, null); - return converter.Chains.AsReadOnly(); + return converter.Chains; } /// @@ -60,7 +60,7 @@ public static IReadOnlyCollection GetRelationshipC /// } /// } /// - public static IncludeExpression FromRelationshipChains(IEnumerable chains) + public static IncludeExpression FromRelationshipChains(IReadOnlyCollection chains) { if (chains == null) { @@ -71,7 +71,7 @@ public static IncludeExpression FromRelationshipChains(IEnumerable ConvertChainsToElements(IEnumerable chains) + private static IReadOnlyCollection ConvertChainsToElements(IReadOnlyCollection chains) { var rootNode = new MutableIncludeNode(null); @@ -90,7 +90,7 @@ private static IReadOnlyCollection ConvertChainsToElem } } - return rootNode.Children.Values.Select(child => child.ToExpression()).ToList(); + return rootNode.Children.Values.Select(child => child.ToExpression()).ToArray(); } private sealed class IncludeToChainsConverter : QueryExpressionVisitor @@ -152,7 +152,7 @@ public MutableIncludeNode(RelationshipAttribute relationship) public IncludeElementExpression ToExpression() { - var elementChildren = Children.Values.Select(child => child.ToExpression()).ToList(); + var elementChildren = Children.Values.Select(child => child.ToExpression()).ToArray(); return new IncludeElementExpression(_relationship, elementChildren); } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs index 48ee45e203..c8ca8c51f1 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries.Expressions { diff --git a/src/JsonApiDotNetCore/Queries/Expressions/NotExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/NotExpression.cs index e17fda583e..14216c0dce 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/NotExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/NotExpression.cs @@ -1,5 +1,5 @@ using System; -using JsonApiDotNetCore.Internal.Queries.Parsing; +using JsonApiDotNetCore.Queries.Internal.Parsing; namespace JsonApiDotNetCore.Queries.Expressions { diff --git a/src/JsonApiDotNetCore/Queries/Expressions/NullConstantExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/NullConstantExpression.cs index 74fdb04835..3e69ec907c 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/NullConstantExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/NullConstantExpression.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore.Internal.Queries.Parsing; +using JsonApiDotNetCore.Queries.Internal.Parsing; namespace JsonApiDotNetCore.Queries.Expressions { diff --git a/src/JsonApiDotNetCore/Queries/Expressions/PaginationExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/PaginationExpression.cs index 89d171929f..3e43d5c429 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/PaginationExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/PaginationExpression.cs @@ -1,4 +1,5 @@ using System; +using JsonApiDotNetCore.Configuration; namespace JsonApiDotNetCore.Queries.Expressions { diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs index 0f14cb87f1..cfe9ad5c73 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs @@ -3,7 +3,7 @@ namespace JsonApiDotNetCore.Queries.Expressions /// /// Implements the visitor design pattern that enables traversing a tree. /// - public class QueryExpressionVisitor + public abstract class QueryExpressionVisitor { public virtual TResult Visit(QueryExpression expression, TArgument argument) { diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs index e7e33ab46a..153630a4e0 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs @@ -1,12 +1,12 @@ using System; using System.Linq; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Resources; using Microsoft.Extensions.Primitives; namespace JsonApiDotNetCore.Queries.Expressions { /// - /// Holds a expression, used for custom query string handlers from s. + /// Holds a expression, used for custom query string handlers from s. /// public class QueryableHandlerExpression : QueryExpression { diff --git a/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs index cfea27049a..7ef9442241 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries.Expressions { diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs index 7910354c58..4e3ae04e73 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries.Expressions { diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs index 781d043417..6d3eaecb20 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs @@ -1,9 +1,9 @@ using System; using System.Linq; using System.Linq.Expressions; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries.Expressions { @@ -11,7 +11,7 @@ public static class SparseFieldSetExpressionExtensions { public static SparseFieldSetExpression Including(this SparseFieldSetExpression sparseFieldSet, Expression> attributeSelector, IResourceGraph resourceGraph) - where TResource : IIdentifiable + where TResource : class, IIdentifiable { if (attributeSelector == null) { @@ -45,7 +45,7 @@ private static SparseFieldSetExpression IncludeAttribute(SparseFieldSetExpressio public static SparseFieldSetExpression Excluding(this SparseFieldSetExpression sparseFieldSet, Expression> attributeSelector, IResourceGraph resourceGraph) - where TResource : IIdentifiable + where TResource : class, IIdentifiable { if (attributeSelector == null) { diff --git a/src/JsonApiDotNetCore/Internal/IPaginationContext.cs b/src/JsonApiDotNetCore/Queries/IPaginationContext.cs similarity index 97% rename from src/JsonApiDotNetCore/Internal/IPaginationContext.cs rename to src/JsonApiDotNetCore/Queries/IPaginationContext.cs index 2e5f817585..9db3a24c9f 100644 --- a/src/JsonApiDotNetCore/Internal/IPaginationContext.cs +++ b/src/JsonApiDotNetCore/Queries/IPaginationContext.cs @@ -1,6 +1,6 @@ using JsonApiDotNetCore.Configuration; -namespace JsonApiDotNetCore.Internal +namespace JsonApiDotNetCore.Queries { /// /// Tracks values used for pagination, which is a combined effort from options, query string parsing and fetching the total number of rows. diff --git a/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs index 95a724d76b..f653787b25 100644 --- a/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; namespace JsonApiDotNetCore.Queries diff --git a/src/JsonApiDotNetCore/Internal/Queries/Parsing/FieldChainRequirements.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FieldChainRequirements.cs similarity index 92% rename from src/JsonApiDotNetCore/Internal/Queries/Parsing/FieldChainRequirements.cs rename to src/JsonApiDotNetCore/Queries/Internal/Parsing/FieldChainRequirements.cs index ef7d634f52..443c63a3c0 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/Parsing/FieldChainRequirements.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FieldChainRequirements.cs @@ -1,7 +1,7 @@ using System; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Internal.Queries.Parsing +namespace JsonApiDotNetCore.Queries.Internal.Parsing { /// /// Used internally when parsing subexpressions in the query string parsers to indicate requirements when resolving a chain of fields. diff --git a/src/JsonApiDotNetCore/Internal/Queries/Parsing/FilterParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs similarity index 98% rename from src/JsonApiDotNetCore/Internal/Queries/Parsing/FilterParser.cs rename to src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs index e7dd99f64d..0f2904fc9f 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/Parsing/FilterParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs @@ -3,12 +3,12 @@ using System.Linq; using System.Reflection; using Humanizer; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Internal.Queries.Parsing +namespace JsonApiDotNetCore.Queries.Internal.Parsing { public class FilterParser : QueryExpressionParser { diff --git a/src/JsonApiDotNetCore/Internal/Queries/Parsing/IncludeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs similarity index 93% rename from src/JsonApiDotNetCore/Internal/Queries/Parsing/IncludeParser.cs rename to src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs index 7896531a99..5510509c97 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/Parsing/IncludeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Internal.Queries.Parsing +namespace JsonApiDotNetCore.Queries.Internal.Parsing { public class IncludeParser : QueryExpressionParser { @@ -54,7 +54,7 @@ protected IncludeExpression ParseInclude(int? maximumDepth) return IncludeChainConverter.FromRelationshipChains(chains); } - private static void ValidateMaximumIncludeDepth(int? maximumDepth, List chains) + private static void ValidateMaximumIncludeDepth(int? maximumDepth, IReadOnlyCollection chains) { if (maximumDepth != null) { diff --git a/src/JsonApiDotNetCore/Internal/Queries/Parsing/Keywords.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/Keywords.cs similarity index 93% rename from src/JsonApiDotNetCore/Internal/Queries/Parsing/Keywords.cs rename to src/JsonApiDotNetCore/Queries/Internal/Parsing/Keywords.cs index 3bea05fe3e..c1933152e1 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/Parsing/Keywords.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/Keywords.cs @@ -1,4 +1,4 @@ -namespace JsonApiDotNetCore.Internal.Queries.Parsing +namespace JsonApiDotNetCore.Queries.Internal.Parsing { public static class Keywords { diff --git a/src/JsonApiDotNetCore/Internal/Queries/Parsing/PaginationParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs similarity index 96% rename from src/JsonApiDotNetCore/Internal/Queries/Parsing/PaginationParser.cs rename to src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs index 7010b2962e..765f62e7b1 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/Parsing/PaginationParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Internal.Queries.Parsing +namespace JsonApiDotNetCore.Queries.Internal.Parsing { public class PaginationParser : QueryExpressionParser { diff --git a/src/JsonApiDotNetCore/Internal/Queries/Parsing/QueryExpressionParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs similarity index 96% rename from src/JsonApiDotNetCore/Internal/Queries/Parsing/QueryExpressionParser.cs rename to src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs index 21e0e65508..277d18f62d 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/Parsing/QueryExpressionParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Internal.Queries.Parsing +namespace JsonApiDotNetCore.Queries.Internal.Parsing { /// /// The base class for parsing query string parameters, using the Recursive Descent algorithm. diff --git a/src/JsonApiDotNetCore/Internal/Queries/Parsing/QueryParseException.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryParseException.cs similarity index 76% rename from src/JsonApiDotNetCore/Internal/Queries/Parsing/QueryParseException.cs rename to src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryParseException.cs index adf73b2da2..33a556e6c2 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/Parsing/QueryParseException.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryParseException.cs @@ -1,6 +1,6 @@ using System; -namespace JsonApiDotNetCore.Internal.Queries.Parsing +namespace JsonApiDotNetCore.Queries.Internal.Parsing { public sealed class QueryParseException : Exception { diff --git a/src/JsonApiDotNetCore/Internal/Queries/Parsing/QueryStringParameterScopeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs similarity index 95% rename from src/JsonApiDotNetCore/Internal/Queries/Parsing/QueryStringParameterScopeParser.cs rename to src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs index 21be71efb3..39a00a3098 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/Parsing/QueryStringParameterScopeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Internal.Queries.Parsing +namespace JsonApiDotNetCore.Queries.Internal.Parsing { public class QueryStringParameterScopeParser : QueryExpressionParser { diff --git a/src/JsonApiDotNetCore/Internal/Queries/Parsing/QueryTokenizer.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryTokenizer.cs similarity index 98% rename from src/JsonApiDotNetCore/Internal/Queries/Parsing/QueryTokenizer.cs rename to src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryTokenizer.cs index 47a302dbdb..25e7659001 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/Parsing/QueryTokenizer.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryTokenizer.cs @@ -3,7 +3,7 @@ using System.Collections.ObjectModel; using System.Text; -namespace JsonApiDotNetCore.Internal.Queries.Parsing +namespace JsonApiDotNetCore.Queries.Internal.Parsing { public sealed class QueryTokenizer { diff --git a/src/JsonApiDotNetCore/Internal/Queries/Parsing/ResourceFieldChainResolver.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs similarity index 98% rename from src/JsonApiDotNetCore/Internal/Queries/Parsing/ResourceFieldChainResolver.cs rename to src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs index 1f0860c4e7..a28236f0e1 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/Parsing/ResourceFieldChainResolver.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Internal.Queries.Parsing +namespace JsonApiDotNetCore.Queries.Internal.Parsing { /// /// Provides helper methods to resolve a chain of fields (relationships and attributes) from the resource graph. diff --git a/src/JsonApiDotNetCore/Internal/Queries/Parsing/SortParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs similarity index 95% rename from src/JsonApiDotNetCore/Internal/Queries/Parsing/SortParser.cs rename to src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs index d5d9751963..d8beac4d8f 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/Parsing/SortParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Internal.Queries.Parsing +namespace JsonApiDotNetCore.Queries.Internal.Parsing { public class SortParser : QueryExpressionParser { diff --git a/src/JsonApiDotNetCore/Internal/Queries/Parsing/SparseFieldSetParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs similarity index 94% rename from src/JsonApiDotNetCore/Internal/Queries/Parsing/SparseFieldSetParser.cs rename to src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs index 562359e412..e2e205ebf8 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/Parsing/SparseFieldSetParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Internal.Queries.Parsing +namespace JsonApiDotNetCore.Queries.Internal.Parsing { public class SparseFieldSetParser : QueryExpressionParser { diff --git a/src/JsonApiDotNetCore/Internal/Queries/Parsing/Token.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/Token.cs similarity index 88% rename from src/JsonApiDotNetCore/Internal/Queries/Parsing/Token.cs rename to src/JsonApiDotNetCore/Queries/Internal/Parsing/Token.cs index c99654aa6e..93995233d2 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/Parsing/Token.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/Token.cs @@ -1,4 +1,4 @@ -namespace JsonApiDotNetCore.Internal.Queries.Parsing +namespace JsonApiDotNetCore.Queries.Internal.Parsing { public sealed class Token { diff --git a/src/JsonApiDotNetCore/Internal/Queries/Parsing/TokenKind.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/TokenKind.cs similarity index 79% rename from src/JsonApiDotNetCore/Internal/Queries/Parsing/TokenKind.cs rename to src/JsonApiDotNetCore/Queries/Internal/Parsing/TokenKind.cs index 9be908fb14..3658c82f18 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/Parsing/TokenKind.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/TokenKind.cs @@ -1,4 +1,4 @@ -namespace JsonApiDotNetCore.Internal.Queries.Parsing +namespace JsonApiDotNetCore.Queries.Internal.Parsing { public enum TokenKind { diff --git a/src/JsonApiDotNetCore/Internal/Queries/QueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs similarity index 86% rename from src/JsonApiDotNetCore/Internal/Queries/QueryLayerComposer.cs rename to src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs index ca05dae17d..95ad1dba01 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/QueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs @@ -2,16 +2,13 @@ using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.Services.Contract; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Internal.Queries +namespace JsonApiDotNetCore.Queries.Internal { - /// + /// public class QueryLayerComposer : IQueryLayerComposer { private readonly IEnumerable _constraintProviders; @@ -34,7 +31,7 @@ public QueryLayerComposer( _paginationContext = paginationContext ?? throw new ArgumentNullException(nameof(paginationContext)); } - /// + /// public FilterExpression GetTopFilter() { var constraints = _constraintProviders.SelectMany(p => p.GetConstraints()).ToArray(); @@ -58,7 +55,7 @@ public FilterExpression GetTopFilter() return new LogicalExpression(LogicalOperator.And, topFilters); } - /// + /// public QueryLayer Compose(ResourceContext requestResource) { if (requestResource == null) @@ -113,7 +110,7 @@ private IncludeExpression ComposeChildren(QueryLayer topLayer, ExpressionInScope } private IReadOnlyCollection ProcessIncludeSet(IReadOnlyCollection includeElements, - QueryLayer parentLayer, IList parentRelationshipChain, ExpressionInScope[] constraints) + QueryLayer parentLayer, ICollection parentRelationshipChain, ExpressionInScope[] constraints) { includeElements = GetIncludeElements(includeElements, parentLayer.ResourceContext) ?? Array.Empty(); @@ -181,6 +178,8 @@ private static IReadOnlyCollection ApplyIncludeElement protected virtual IReadOnlyCollection GetIncludeElements(IReadOnlyCollection includeElements, ResourceContext resourceContext) { + if (resourceContext == null) throw new ArgumentNullException(nameof(resourceContext)); + var resourceDefinition = _resourceDefinitionProvider.Get(resourceContext.ResourceType); if (resourceDefinition != null) { @@ -190,8 +189,11 @@ protected virtual IReadOnlyCollection GetIncludeElemen return includeElements; } - protected virtual FilterExpression GetFilter(IEnumerable expressionsInScope, ResourceContext resourceContext) + protected virtual FilterExpression GetFilter(IReadOnlyCollection expressionsInScope, ResourceContext resourceContext) { + if (expressionsInScope == null) throw new ArgumentNullException(nameof(expressionsInScope)); + if (resourceContext == null) throw new ArgumentNullException(nameof(resourceContext)); + var filters = expressionsInScope.OfType().ToArray(); var filter = filters.Length > 1 ? new LogicalExpression(LogicalOperator.And, filters) : filters.FirstOrDefault(); @@ -204,8 +206,11 @@ protected virtual FilterExpression GetFilter(IEnumerable expres return filter; } - protected virtual SortExpression GetSort(IEnumerable expressionsInScope, ResourceContext resourceContext) + protected virtual SortExpression GetSort(IReadOnlyCollection expressionsInScope, ResourceContext resourceContext) { + if (expressionsInScope == null) throw new ArgumentNullException(nameof(expressionsInScope)); + if (resourceContext == null) throw new ArgumentNullException(nameof(resourceContext)); + var sort = expressionsInScope.OfType().FirstOrDefault(); var resourceDefinition = _resourceDefinitionProvider.Get(resourceContext.ResourceType); @@ -223,8 +228,11 @@ protected virtual SortExpression GetSort(IEnumerable expression return sort; } - protected virtual PaginationExpression GetPagination(IEnumerable expressionsInScope, ResourceContext resourceContext) + protected virtual PaginationExpression GetPagination(IReadOnlyCollection expressionsInScope, ResourceContext resourceContext) { + if (expressionsInScope == null) throw new ArgumentNullException(nameof(expressionsInScope)); + if (resourceContext == null) throw new ArgumentNullException(nameof(resourceContext)); + var pagination = expressionsInScope.OfType().FirstOrDefault(); var resourceDefinition = _resourceDefinitionProvider.Get(resourceContext.ResourceType); @@ -238,8 +246,11 @@ protected virtual PaginationExpression GetPagination(IEnumerable GetSparseFieldSetProjection(IEnumerable expressionsInScope, ResourceContext resourceContext) + protected virtual IDictionary GetSparseFieldSetProjection(IReadOnlyCollection expressionsInScope, ResourceContext resourceContext) { + if (expressionsInScope == null) throw new ArgumentNullException(nameof(expressionsInScope)); + if (resourceContext == null) throw new ArgumentNullException(nameof(resourceContext)); + var attributes = expressionsInScope.OfType().SelectMany(sparseFieldSet => sparseFieldSet.Attributes).ToHashSet(); var resourceDefinition = _resourceDefinitionProvider.Get(resourceContext.ResourceType); diff --git a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/IncludeClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs similarity index 95% rename from src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/IncludeClauseBuilder.cs rename to src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs index 6c169cf8a1..09a6f42655 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/IncludeClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs @@ -2,12 +2,12 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources.Annotations; using Microsoft.EntityFrameworkCore; -namespace JsonApiDotNetCore.Internal.Queries.QueryableBuilding +namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { /// /// Transforms into calls. diff --git a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/LambdaParameterNameFactory.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaParameterNameFactory.cs similarity index 95% rename from src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/LambdaParameterNameFactory.cs rename to src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaParameterNameFactory.cs index 438eaebd78..341dd2a23e 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/LambdaParameterNameFactory.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaParameterNameFactory.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using Humanizer; -namespace JsonApiDotNetCore.Internal.Queries.QueryableBuilding +namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { /// /// Produces unique names for lambda parameters. diff --git a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/LambdaParameterNameScope.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaParameterNameScope.cs similarity index 89% rename from src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/LambdaParameterNameScope.cs rename to src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaParameterNameScope.cs index 30e1958e15..e443997ca1 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/LambdaParameterNameScope.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaParameterNameScope.cs @@ -1,6 +1,6 @@ using System; -namespace JsonApiDotNetCore.Internal.Queries.QueryableBuilding +namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { public sealed class LambdaParameterNameScope : IDisposable { diff --git a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/LambdaScope.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScope.cs similarity index 81% rename from src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/LambdaScope.cs rename to src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScope.cs index 0ed434237d..8555fffc43 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/LambdaScope.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScope.cs @@ -1,8 +1,8 @@ using System; using System.Linq.Expressions; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Internal.Queries.QueryableBuilding +namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { /// /// Contains details on a lambda expression, such as the name of the selector "x" in "x => x.Name". @@ -17,6 +17,9 @@ public sealed class LambdaScope : IDisposable public LambdaScope(LambdaParameterNameFactory nameFactory, Type elementType, Expression accessorExpression, HasManyThroughAttribute hasManyThrough) { + if (nameFactory == null) throw new ArgumentNullException(nameof(nameFactory)); + if (elementType == null) throw new ArgumentNullException(nameof(elementType)); + _parameterNameScope = nameFactory.Create(elementType.Name); Parameter = Expression.Parameter(elementType, _parameterNameScope.Name); diff --git a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/LambdaScopeFactory.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScopeFactory.cs similarity index 88% rename from src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/LambdaScopeFactory.cs rename to src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScopeFactory.cs index 47fdf29ed3..b0ae0467b8 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/LambdaScopeFactory.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/LambdaScopeFactory.cs @@ -1,8 +1,8 @@ using System; using System.Linq.Expressions; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Internal.Queries.QueryableBuilding +namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { public sealed class LambdaScopeFactory { diff --git a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/OrderClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/OrderClauseBuilder.cs similarity index 93% rename from src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/OrderClauseBuilder.cs rename to src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/OrderClauseBuilder.cs index 0287295d94..2d7d72331e 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/OrderClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/OrderClauseBuilder.cs @@ -2,13 +2,13 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using JsonApiDotNetCore.Models.Annotation; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Internal.Queries.QueryableBuilding +namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { /// - /// Transforms into calls. + /// Transforms into calls. /// public class OrderClauseBuilder : QueryClauseBuilder { @@ -73,7 +73,7 @@ protected override MemberExpression CreatePropertyExpressionForFieldChain(IReadO { var components = chain.Select(field => // In case of a HasManyThrough access (from count() function), we only need to look at the number of entries in the join table. - field is HasManyThroughAttribute hasManyThrough ? hasManyThrough.ThroughProperty.Name : field.Property.Name).ToList(); + field is HasManyThroughAttribute hasManyThrough ? hasManyThrough.ThroughProperty.Name : field.Property.Name).ToArray(); return CreatePropertyExpressionFromComponents(LambdaScope.Accessor, components); } diff --git a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/QueryClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs similarity index 91% rename from src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/QueryClauseBuilder.cs rename to src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs index 64eabbf2a5..701a867812 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/QueryClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs @@ -3,11 +3,10 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Models.Annotation; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Internal.Queries.QueryableBuilding +namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { /// /// Base class for transforming trees into system trees. @@ -39,7 +38,10 @@ private static Expression TryGetCollectionCount(Expression collectionExpression) var properties = new HashSet(collectionExpression.Type.GetProperties()); if (collectionExpression.Type.IsInterface) { - properties.AddRange(collectionExpression.Type.GetInterfaces().SelectMany(i => i.GetProperties())); + foreach (var item in collectionExpression.Type.GetInterfaces().SelectMany(i => i.GetProperties())) + { + properties.Add(item); + } } foreach (var property in properties) @@ -61,12 +63,12 @@ public override Expression VisitResourceFieldChain(ResourceFieldChainExpression protected virtual MemberExpression CreatePropertyExpressionForFieldChain(IReadOnlyCollection chain, Expression source) { var components = chain.Select(field => - field is RelationshipAttribute relationship ? relationship.RelationshipPath : field.Property.Name); + field is RelationshipAttribute relationship ? relationship.RelationshipPath : field.Property.Name).ToArray(); return CreatePropertyExpressionFromComponents(source, components); } - protected static MemberExpression CreatePropertyExpressionFromComponents(Expression source, IEnumerable components) + protected static MemberExpression CreatePropertyExpressionFromComponents(Expression source, IReadOnlyCollection components) { MemberExpression property = null; diff --git a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/QueryableBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs similarity index 94% rename from src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/QueryableBuilder.cs rename to src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs index 0e92d113cb..ac64c6c15e 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/QueryableBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs @@ -2,13 +2,13 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; using Microsoft.EntityFrameworkCore.Metadata; -namespace JsonApiDotNetCore.Internal.Queries.QueryableBuilding +namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { /// /// Drives conversion from into system trees. @@ -40,6 +40,8 @@ public QueryableBuilder(Expression source, Type elementType, Type extensionType, public Expression ApplyQuery(QueryLayer layer) { + if (layer == null) throw new ArgumentNullException(nameof(layer)); + Expression expression = _source; if (layer.Include != null) diff --git a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/SelectClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs similarity index 97% rename from src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/SelectClauseBuilder.cs rename to src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs index ded1dad414..3d45eafdb0 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/SelectClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs @@ -3,17 +3,17 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; -namespace JsonApiDotNetCore.Internal.Queries.QueryableBuilding +namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { /// - /// Transforms into calls. + /// Transforms into calls. /// public class SelectClauseBuilder : QueryClauseBuilder { @@ -174,7 +174,7 @@ private Expression CreateCollectionInitializer(LambdaScope lambdaScope, Property Expression layerExpression = builder.ApplyQuery(layer); Type enumerableOfElementType = typeof(IEnumerable<>).MakeGenericType(elementType); - Type typedCollection = collectionProperty.PropertyType.ToConcreteCollectionType(); + Type typedCollection = TypeHelper.ToConcreteCollectionType(collectionProperty.PropertyType); ConstructorInfo typedCollectionConstructor = typedCollection.GetConstructor(new[] { diff --git a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/SkipTakeClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SkipTakeClauseBuilder.cs similarity index 97% rename from src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/SkipTakeClauseBuilder.cs rename to src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SkipTakeClauseBuilder.cs index 8e693117a4..96c94a9cb6 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/SkipTakeClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SkipTakeClauseBuilder.cs @@ -3,7 +3,7 @@ using System.Linq.Expressions; using JsonApiDotNetCore.Queries.Expressions; -namespace JsonApiDotNetCore.Internal.Queries.QueryableBuilding +namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { /// /// Transforms into and calls. diff --git a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/WhereClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/WhereClauseBuilder.cs similarity index 97% rename from src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/WhereClauseBuilder.cs rename to src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/WhereClauseBuilder.cs index 19f8158c2c..9ed848053f 100644 --- a/src/JsonApiDotNetCore/Internal/Queries/QueryableBuilding/WhereClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/WhereClauseBuilder.cs @@ -3,14 +3,14 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Internal.Queries.QueryableBuilding +namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { /// - /// Transforms into calls. + /// Transforms into calls. /// public class WhereClauseBuilder : QueryClauseBuilder { @@ -280,7 +280,7 @@ protected override MemberExpression CreatePropertyExpressionForFieldChain(IReadO { var components = chain.Select(field => // In case of a HasManyThrough access (from count() or has() function), we only need to look at the number of entries in the join table. - field is HasManyThroughAttribute hasManyThrough ? hasManyThrough.ThroughProperty.Name : field.Property.Name).ToList(); + field is HasManyThroughAttribute hasManyThrough ? hasManyThrough.ThroughProperty.Name : field.Property.Name).ToArray(); return CreatePropertyExpressionFromComponents(LambdaScope.Accessor, components); } diff --git a/src/JsonApiDotNetCore/Internal/PaginationContext.cs b/src/JsonApiDotNetCore/Queries/PaginationContext.cs similarity index 68% rename from src/JsonApiDotNetCore/Internal/PaginationContext.cs rename to src/JsonApiDotNetCore/Queries/PaginationContext.cs index 5a7b2dece6..fbdd8ad453 100644 --- a/src/JsonApiDotNetCore/Internal/PaginationContext.cs +++ b/src/JsonApiDotNetCore/Queries/PaginationContext.cs @@ -1,20 +1,21 @@ using System; +using JsonApiDotNetCore.Configuration; -namespace JsonApiDotNetCore.Internal +namespace JsonApiDotNetCore.Queries { - /// + /// internal sealed class PaginationContext : IPaginationContext { - /// + /// public PageNumber PageNumber { get; set; } - /// + /// public PageSize PageSize { get; set; } - /// + /// public int? TotalResourceCount { get; set; } - /// + /// public int? TotalPageCount => TotalResourceCount == null || PageSize == null ? null : (int?) Math.Ceiling((decimal) TotalResourceCount.Value / PageSize.Value); diff --git a/src/JsonApiDotNetCore/Queries/QueryLayer.cs b/src/JsonApiDotNetCore/Queries/QueryLayer.cs index 72a2ae5ae1..74d8d5f043 100644 --- a/src/JsonApiDotNetCore/Queries/QueryLayer.cs +++ b/src/JsonApiDotNetCore/Queries/QueryLayer.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Queries { diff --git a/src/JsonApiDotNetCore/QueryStrings/IDefaultsQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/IDefaultsQueryStringParameterReader.cs index 4ac078d38d..176bf69bda 100644 --- a/src/JsonApiDotNetCore/QueryStrings/IDefaultsQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/IDefaultsQueryStringParameterReader.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.QueryStrings public interface IDefaultsQueryStringParameterReader : IQueryStringParameterReader { /// - /// Contains the effective value of default configuration and query string override, after parsing has occured. + /// Contains the effective value of default configuration and query string override, after parsing has occurred. /// DefaultValueHandling SerializerDefaultValueHandling { get; } } diff --git a/src/JsonApiDotNetCore/QueryStrings/INullsQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/INullsQueryStringParameterReader.cs index d4a403bd7e..e1885925e5 100644 --- a/src/JsonApiDotNetCore/QueryStrings/INullsQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/INullsQueryStringParameterReader.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.QueryStrings public interface INullsQueryStringParameterReader : IQueryStringParameterReader { /// - /// Contains the effective value of default configuration and query string override, after parsing has occured. + /// Contains the effective value of default configuration and query string override, after parsing has occurred. /// NullValueHandling SerializerNullValueHandling { get; } } diff --git a/src/JsonApiDotNetCore/QueryStrings/IQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/IQueryStringParameterReader.cs index 88c7d8b03d..b07399b98c 100644 --- a/src/JsonApiDotNetCore/QueryStrings/IQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/IQueryStringParameterReader.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Controllers.Annotations; using Microsoft.Extensions.Primitives; namespace JsonApiDotNetCore.QueryStrings @@ -9,9 +9,9 @@ namespace JsonApiDotNetCore.QueryStrings public interface IQueryStringParameterReader { /// - /// Indicates whether usage of this query string parameter is blocked using on a controller. + /// Indicates whether usage of this query string parameter is blocked using on a controller. /// - bool IsEnabled(DisableQueryAttribute disableQueryAttribute); + bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute); /// /// Indicates whether this reader can handle the specified query string parameter. diff --git a/src/JsonApiDotNetCore/QueryStrings/IQueryStringReader.cs b/src/JsonApiDotNetCore/QueryStrings/IQueryStringReader.cs index 0a9743f961..c26b14bd1e 100644 --- a/src/JsonApiDotNetCore/QueryStrings/IQueryStringReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/IQueryStringReader.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Controllers.Annotations; namespace JsonApiDotNetCore.QueryStrings { @@ -10,9 +10,9 @@ public interface IQueryStringReader /// /// Reads and processes the key/value pairs from the request query string. /// - /// - /// The if set on the controller that is targeted by the current request. + /// + /// The if set on the controller that is targeted by the current request. /// - void ReadAll(DisableQueryAttribute disableQueryAttribute); + void ReadAll(DisableQueryStringAttribute disableQueryStringAttribute); } } diff --git a/src/JsonApiDotNetCore/QueryStrings/IResourceDefinitionQueryableParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/IResourceDefinitionQueryableParameterReader.cs index df4ea75d61..d2531b6918 100644 --- a/src/JsonApiDotNetCore/QueryStrings/IResourceDefinitionQueryableParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/IResourceDefinitionQueryableParameterReader.cs @@ -1,5 +1,5 @@ -using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Resources; namespace JsonApiDotNetCore.QueryStrings { diff --git a/src/JsonApiDotNetCore/Internal/QueryStrings/DefaultsQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs similarity index 58% rename from src/JsonApiDotNetCore/Internal/QueryStrings/DefaultsQueryStringParameterReader.cs rename to src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs index 3279347c0b..776b9559cb 100644 --- a/src/JsonApiDotNetCore/Internal/QueryStrings/DefaultsQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs @@ -1,41 +1,43 @@ +using System; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.QueryStrings; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Errors; using Microsoft.Extensions.Primitives; using Newtonsoft.Json; -namespace JsonApiDotNetCore.Internal.QueryStrings +namespace JsonApiDotNetCore.QueryStrings.Internal { - /// + /// public class DefaultsQueryStringParameterReader : IDefaultsQueryStringParameterReader { private readonly IJsonApiOptions _options; - /// + /// public DefaultValueHandling SerializerDefaultValueHandling { get; private set; } public DefaultsQueryStringParameterReader(IJsonApiOptions options) { + _options = options ?? throw new ArgumentNullException(nameof(options)); SerializerDefaultValueHandling = options.SerializerSettings.DefaultValueHandling; - _options = options; } - /// - public bool IsEnabled(DisableQueryAttribute disableQueryAttribute) + /// + public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) { + if (disableQueryStringAttribute == null) throw new ArgumentNullException(nameof(disableQueryStringAttribute)); + return _options.AllowQueryStringOverrideForSerializerDefaultValueHandling && - !disableQueryAttribute.ContainsParameter(StandardQueryStringParameters.Defaults); + !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Defaults); } - /// - public bool CanRead(string parameterName) + /// + public virtual bool CanRead(string parameterName) { return parameterName == "defaults"; } - /// - public void Read(string parameterName, StringValues parameterValue) + /// + public virtual void Read(string parameterName, StringValues parameterValue) { if (!bool.TryParse(parameterValue, out var result)) { diff --git a/src/JsonApiDotNetCore/Internal/QueryStrings/FilterQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs similarity index 77% rename from src/JsonApiDotNetCore/Internal/QueryStrings/FilterQueryStringParameterReader.cs rename to src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs index 577d4950fd..87bb7f3554 100644 --- a/src/JsonApiDotNetCore/Internal/QueryStrings/FilterQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs @@ -2,19 +2,17 @@ using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Internal.Queries.Parsing; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.QueryStrings; -using JsonApiDotNetCore.RequestServices.Contracts; +using JsonApiDotNetCore.Queries.Internal.Parsing; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; using Microsoft.Extensions.Primitives; -namespace JsonApiDotNetCore.Internal.QueryStrings +namespace JsonApiDotNetCore.QueryStrings.Internal { public class FilterQueryStringParameterReader : QueryStringParameterReader, IFilterQueryStringParameterReader { @@ -28,16 +26,16 @@ public class FilterQueryStringParameterReader : QueryStringParameterReader, IFil private readonly Dictionary> _filtersPerScope = new Dictionary>(); private string _lastParameterName; - public FilterQueryStringParameterReader(ICurrentRequest currentRequest, + public FilterQueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory, IJsonApiOptions options) - : base(currentRequest, resourceContextProvider) + : base(request, resourceContextProvider) { _options = options ?? throw new ArgumentNullException(nameof(options)); _scopeParser = new QueryStringParameterScopeParser(resourceContextProvider, FieldChainRequirements.EndsInToMany); _filterParser = new FilterParser(resourceContextProvider, resourceFactory, ValidateSingleField); } - private void ValidateSingleField(ResourceFieldAttribute field, ResourceContext resourceContext, string path) + protected void ValidateSingleField(ResourceFieldAttribute field, ResourceContext resourceContext, string path) { if (field is AttrAttribute attribute && !attribute.Capabilities.HasFlag(AttrCapabilities.AllowFilter)) { @@ -46,21 +44,23 @@ private void ValidateSingleField(ResourceFieldAttribute field, ResourceContext r } } - /// - public bool IsEnabled(DisableQueryAttribute disableQueryAttribute) + /// + public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) { - return !disableQueryAttribute.ContainsParameter(StandardQueryStringParameters.Filter); + if (disableQueryStringAttribute == null) throw new ArgumentNullException(nameof(disableQueryStringAttribute)); + + return !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Filter); } - /// - public bool CanRead(string parameterName) + /// + public virtual bool CanRead(string parameterName) { - var isNested = parameterName.StartsWith("filter[") && parameterName.EndsWith("]"); + var isNested = parameterName.StartsWith("filter[", StringComparison.Ordinal) && parameterName.EndsWith("]", StringComparison.Ordinal); return parameterName == "filter" || isNested; } - /// - public void Read(string parameterName, StringValues parameterValues) + /// + public virtual void Read(string parameterName, StringValues parameterValues) { _lastParameterName = parameterName; @@ -125,10 +125,10 @@ private void StoreFilterInScope(FilterExpression filter, ResourceFieldChainExpre } } - /// - public IReadOnlyCollection GetConstraints() + /// + public virtual IReadOnlyCollection GetConstraints() { - return EnumerateFiltersInScopes().ToList().AsReadOnly(); + return EnumerateFiltersInScopes().ToArray(); } private IEnumerable EnumerateFiltersInScopes() diff --git a/src/JsonApiDotNetCore/Internal/QueryStrings/IncludeQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs similarity index 64% rename from src/JsonApiDotNetCore/Internal/QueryStrings/IncludeQueryStringParameterReader.cs rename to src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs index 4940cb3876..c6d2767ad6 100644 --- a/src/JsonApiDotNetCore/Internal/QueryStrings/IncludeQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs @@ -1,18 +1,16 @@ using System; using System.Collections.Generic; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Internal.Queries.Parsing; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.QueryStrings; -using JsonApiDotNetCore.RequestServices.Contracts; +using JsonApiDotNetCore.Queries.Internal.Parsing; +using JsonApiDotNetCore.Resources.Annotations; using Microsoft.Extensions.Primitives; -namespace JsonApiDotNetCore.Internal.QueryStrings +namespace JsonApiDotNetCore.QueryStrings.Internal { public class IncludeQueryStringParameterReader : QueryStringParameterReader, IIncludeQueryStringParameterReader { @@ -22,14 +20,14 @@ public class IncludeQueryStringParameterReader : QueryStringParameterReader, IIn private IncludeExpression _includeExpression; private string _lastParameterName; - public IncludeQueryStringParameterReader(ICurrentRequest currentRequest, IResourceContextProvider resourceContextProvider, IJsonApiOptions options) - : base(currentRequest, resourceContextProvider) + public IncludeQueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider, IJsonApiOptions options) + : base(request, resourceContextProvider) { _options = options ?? throw new ArgumentNullException(nameof(options)); _includeParser = new IncludeParser(resourceContextProvider, ValidateSingleRelationship); } - private void ValidateSingleRelationship(RelationshipAttribute relationship, ResourceContext resourceContext, string path) + protected void ValidateSingleRelationship(RelationshipAttribute relationship, ResourceContext resourceContext, string path) { if (!relationship.CanInclude) { @@ -41,20 +39,22 @@ private void ValidateSingleRelationship(RelationshipAttribute relationship, Reso } } - /// - public bool IsEnabled(DisableQueryAttribute disableQueryAttribute) + /// + public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) { - return !disableQueryAttribute.ContainsParameter(StandardQueryStringParameters.Include); + if (disableQueryStringAttribute == null) throw new ArgumentNullException(nameof(disableQueryStringAttribute)); + + return !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Include); } - /// - public bool CanRead(string parameterName) + /// + public virtual bool CanRead(string parameterName) { return parameterName == "include"; } - /// - public void Read(string parameterName, StringValues parameterValue) + /// + public virtual void Read(string parameterName, StringValues parameterValue) { _lastParameterName = parameterName; @@ -74,8 +74,8 @@ private IncludeExpression GetInclude(string parameterValue) return _includeParser.Parse(parameterValue, RequestResource, _options.MaximumIncludeDepth); } - /// - public IReadOnlyCollection GetConstraints() + /// + public virtual IReadOnlyCollection GetConstraints() { var expressionInScope = _includeExpression != null ? new ExpressionInScope(null, _includeExpression) diff --git a/src/JsonApiDotNetCore/Internal/QueryStrings/LegacyFilterNotationConverter.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/LegacyFilterNotationConverter.cs similarity index 55% rename from src/JsonApiDotNetCore/Internal/QueryStrings/LegacyFilterNotationConverter.cs rename to src/JsonApiDotNetCore/QueryStrings/Internal/LegacyFilterNotationConverter.cs index ed950d0928..6234625424 100644 --- a/src/JsonApiDotNetCore/Internal/QueryStrings/LegacyFilterNotationConverter.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/LegacyFilterNotationConverter.cs @@ -1,18 +1,19 @@ +using System; using System.Collections.Generic; -using JsonApiDotNetCore.Internal.Queries.Parsing; +using JsonApiDotNetCore.Queries.Internal.Parsing; -namespace JsonApiDotNetCore.Internal.QueryStrings +namespace JsonApiDotNetCore.QueryStrings.Internal { public sealed class LegacyFilterNotationConverter { - private const string _parameterNamePrefix = "filter["; - private const string _parameterNameSuffix = "]"; - private const string _outputParameterName = "filter"; + private const string ParameterNamePrefix = "filter["; + private const string ParameterNameSuffix = "]"; + private const string OutputParameterName = "filter"; - private const string _expressionPrefix = "expr:"; - private const string _notEqualsPrefix = "ne:"; - private const string _inPrefix = "in:"; - private const string _notInPrefix = "nin:"; + private const string ExpressionPrefix = "expr:"; + private const string NotEqualsPrefix = "ne:"; + private const string InPrefix = "in:"; + private const string NotInPrefix = "nin:"; private static readonly Dictionary _prefixConversionTable = new Dictionary { @@ -26,9 +27,12 @@ public sealed class LegacyFilterNotationConverter public (string parameterName, string parameterValue) Convert(string parameterName, string parameterValue) { - if (parameterValue.StartsWith(_expressionPrefix)) + if (parameterName == null) throw new ArgumentNullException(nameof(parameterName)); + if (parameterValue == null) throw new ArgumentNullException(nameof(parameterValue)); + + if (parameterValue.StartsWith(ExpressionPrefix, StringComparison.Ordinal)) { - string expression = parameterValue.Substring(_expressionPrefix.Length); + string expression = parameterValue.Substring(ExpressionPrefix.Length); return (parameterName, expression); } @@ -36,69 +40,69 @@ public sealed class LegacyFilterNotationConverter foreach (var (prefix, keyword) in _prefixConversionTable) { - if (parameterValue.StartsWith(prefix)) + if (parameterValue.StartsWith(prefix, StringComparison.Ordinal)) { var value = parameterValue.Substring(prefix.Length); string escapedValue = EscapeQuotes(value); string expression = $"{keyword}({attributeName},'{escapedValue}')"; - return (_outputParameterName, expression); + return (OutputParameterName, expression); } } - if (parameterValue.StartsWith(_notEqualsPrefix)) + if (parameterValue.StartsWith(NotEqualsPrefix, StringComparison.Ordinal)) { - var value = parameterValue.Substring(_notEqualsPrefix.Length); + var value = parameterValue.Substring(NotEqualsPrefix.Length); string escapedValue = EscapeQuotes(value); string expression = $"{Keywords.Not}({Keywords.Equals}({attributeName},'{escapedValue}'))"; - return (_outputParameterName, expression); + return (OutputParameterName, expression); } - if (parameterValue.StartsWith(_inPrefix)) + if (parameterValue.StartsWith(InPrefix, StringComparison.Ordinal)) { - string[] valueParts = parameterValue.Substring(_inPrefix.Length).Split(","); + string[] valueParts = parameterValue.Substring(InPrefix.Length).Split(","); var valueList = "'" + string.Join("','", valueParts) + "'"; string expression = $"{Keywords.Any}({attributeName},{valueList})"; - return (_outputParameterName, expression); + return (OutputParameterName, expression); } - if (parameterValue.StartsWith(_notInPrefix)) + if (parameterValue.StartsWith(NotInPrefix, StringComparison.Ordinal)) { - string[] valueParts = parameterValue.Substring(_notInPrefix.Length).Split(","); + string[] valueParts = parameterValue.Substring(NotInPrefix.Length).Split(","); var valueList = "'" + string.Join("','", valueParts) + "'"; string expression = $"{Keywords.Not}({Keywords.Any}({attributeName},{valueList}))"; - return (_outputParameterName, expression); + return (OutputParameterName, expression); } if (parameterValue == "isnull:") { string expression = $"{Keywords.Equals}({attributeName},null)"; - return (_outputParameterName, expression); + return (OutputParameterName, expression); } if (parameterValue == "isnotnull:") { string expression = $"{Keywords.Not}({Keywords.Equals}({attributeName},null))"; - return (_outputParameterName, expression); + return (OutputParameterName, expression); } { string escapedValue = EscapeQuotes(parameterValue); string expression = $"{Keywords.Equals}({attributeName},'{escapedValue}')"; - return (_outputParameterName, expression); + return (OutputParameterName, expression); } } private static string ExtractAttributeName(string parameterName) { - if (parameterName.StartsWith(_parameterNamePrefix) && parameterName.EndsWith(_parameterNameSuffix)) + if (parameterName.StartsWith(ParameterNamePrefix, StringComparison.Ordinal) && parameterName.EndsWith(ParameterNameSuffix, StringComparison.Ordinal)) { - string attributeName = parameterName.Substring(_parameterNamePrefix.Length, - parameterName.Length - _parameterNamePrefix.Length - _parameterNameSuffix.Length); + string attributeName = parameterName.Substring(ParameterNamePrefix.Length, + parameterName.Length - ParameterNamePrefix.Length - ParameterNameSuffix.Length); if (attributeName.Length > 0) { diff --git a/src/JsonApiDotNetCore/Internal/QueryStrings/NullsQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs similarity index 57% rename from src/JsonApiDotNetCore/Internal/QueryStrings/NullsQueryStringParameterReader.cs rename to src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs index abf25b1c64..f181f5777b 100644 --- a/src/JsonApiDotNetCore/Internal/QueryStrings/NullsQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs @@ -1,41 +1,43 @@ +using System; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.QueryStrings; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Errors; using Microsoft.Extensions.Primitives; using Newtonsoft.Json; -namespace JsonApiDotNetCore.Internal.QueryStrings +namespace JsonApiDotNetCore.QueryStrings.Internal { - /// + /// public class NullsQueryStringParameterReader : INullsQueryStringParameterReader { private readonly IJsonApiOptions _options; - /// + /// public NullValueHandling SerializerNullValueHandling { get; private set; } public NullsQueryStringParameterReader(IJsonApiOptions options) { + _options = options ?? throw new ArgumentNullException(nameof(options)); SerializerNullValueHandling = options.SerializerSettings.NullValueHandling; - _options = options; } - /// - public bool IsEnabled(DisableQueryAttribute disableQueryAttribute) + /// + public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) { + if (disableQueryStringAttribute == null) throw new ArgumentNullException(nameof(disableQueryStringAttribute)); + return _options.AllowQueryStringOverrideForSerializerNullValueHandling && - !disableQueryAttribute.ContainsParameter(StandardQueryStringParameters.Nulls); + !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Nulls); } - /// - public bool CanRead(string parameterName) + /// + public virtual bool CanRead(string parameterName) { return parameterName == "nulls"; } - /// - public void Read(string parameterName, StringValues parameterValue) + /// + public virtual void Read(string parameterName, StringValues parameterValue) { if (!bool.TryParse(parameterValue, out var result)) { diff --git a/src/JsonApiDotNetCore/Internal/QueryStrings/PaginationQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs similarity index 79% rename from src/JsonApiDotNetCore/Internal/QueryStrings/PaginationQueryStringParameterReader.cs rename to src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs index 9d9afcaa14..b7b8148971 100644 --- a/src/JsonApiDotNetCore/Internal/QueryStrings/PaginationQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs @@ -2,22 +2,20 @@ using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Internal.Queries.Parsing; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.QueryStrings; -using JsonApiDotNetCore.RequestServices.Contracts; +using JsonApiDotNetCore.Queries.Internal.Parsing; using Microsoft.Extensions.Primitives; -namespace JsonApiDotNetCore.Internal.QueryStrings +namespace JsonApiDotNetCore.QueryStrings.Internal { public class PaginationQueryStringParameterReader : QueryStringParameterReader, IPaginationQueryStringParameterReader { - private const string _pageSizeParameterName = "page[size]"; - private const string _pageNumberParameterName = "page[number]"; + private const string PageSizeParameterName = "page[size]"; + private const string PageNumberParameterName = "page[number]"; private readonly IJsonApiOptions _options; private readonly PaginationParser _paginationParser; @@ -25,27 +23,29 @@ public class PaginationQueryStringParameterReader : QueryStringParameterReader, private PaginationQueryStringValueExpression _pageSizeConstraint; private PaginationQueryStringValueExpression _pageNumberConstraint; - public PaginationQueryStringParameterReader(ICurrentRequest currentRequest, IResourceContextProvider resourceContextProvider, IJsonApiOptions options) - : base(currentRequest, resourceContextProvider) + public PaginationQueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider, IJsonApiOptions options) + : base(request, resourceContextProvider) { _options = options ?? throw new ArgumentNullException(nameof(options)); _paginationParser = new PaginationParser(resourceContextProvider); } - /// - public bool IsEnabled(DisableQueryAttribute disableQueryAttribute) + /// + public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) { - return !disableQueryAttribute.ContainsParameter(StandardQueryStringParameters.Page); + if (disableQueryStringAttribute == null) throw new ArgumentNullException(nameof(disableQueryStringAttribute)); + + return !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Page); } - /// - public bool CanRead(string parameterName) + /// + public virtual bool CanRead(string parameterName) { - return parameterName == _pageSizeParameterName || parameterName == _pageNumberParameterName; + return parameterName == PageSizeParameterName || parameterName == PageNumberParameterName; } - /// - public void Read(string parameterName, StringValues parameterValue) + /// + public virtual void Read(string parameterName, StringValues parameterValue) { try { @@ -56,7 +56,7 @@ public void Read(string parameterName, StringValues parameterValue) AssertIsCollectionRequest(); } - if (parameterName == _pageSizeParameterName) + if (parameterName == PageSizeParameterName) { ValidatePageSize(constraint); _pageSizeConstraint = constraint; @@ -78,7 +78,7 @@ private PaginationQueryStringValueExpression GetPageConstraint(string parameterV return _paginationParser.Parse(parameterValue, RequestResource); } - private void ValidatePageSize(PaginationQueryStringValueExpression constraint) + protected virtual void ValidatePageSize(PaginationQueryStringValueExpression constraint) { if (_options.MaximumPageSize != null) { @@ -99,7 +99,7 @@ private void ValidatePageSize(PaginationQueryStringValueExpression constraint) } } - private void ValidatePageNumber(PaginationQueryStringValueExpression constraint) + protected virtual void ValidatePageNumber(PaginationQueryStringValueExpression constraint) { if (_options.MaximumPageNumber != null && constraint.Elements.Any(element => element.Value > _options.MaximumPageNumber.OneBasedValue)) @@ -113,8 +113,8 @@ private void ValidatePageNumber(PaginationQueryStringValueExpression constraint) } } - /// - public IReadOnlyCollection GetConstraints() + /// + public virtual IReadOnlyCollection GetConstraints() { var context = new PaginationContext(); @@ -178,7 +178,7 @@ private void ApplyOptionsInEntry(MutablePaginationEntry entry, IJsonApiOptions o public IReadOnlyCollection GetExpressionsInScope() { - return EnumerateExpressionsInScope().ToList(); + return EnumerateExpressionsInScope().ToArray(); } private IEnumerable EnumerateExpressionsInScope() diff --git a/src/JsonApiDotNetCore/Internal/QueryStrings/QueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs similarity index 66% rename from src/JsonApiDotNetCore/Internal/QueryStrings/QueryStringParameterReader.cs rename to src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs index a8814ef445..453565eab5 100644 --- a/src/JsonApiDotNetCore/Internal/QueryStrings/QueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs @@ -1,12 +1,12 @@ using System; using System.Linq; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Internal.Queries.Parsing; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.RequestServices.Contracts; +using JsonApiDotNetCore.Queries.Internal.Parsing; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Internal.QueryStrings +namespace JsonApiDotNetCore.QueryStrings.Internal { public abstract class QueryStringParameterReader { @@ -15,16 +15,16 @@ public abstract class QueryStringParameterReader protected ResourceContext RequestResource { get; } - protected QueryStringParameterReader(ICurrentRequest currentRequest, IResourceContextProvider resourceContextProvider) + protected QueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider) { - if (currentRequest == null) + if (request == null) { - throw new ArgumentNullException(nameof(currentRequest)); + throw new ArgumentNullException(nameof(request)); } _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); - _isCollectionRequest = currentRequest.IsCollection; - RequestResource = currentRequest.SecondaryResource ?? currentRequest.PrimaryResource; + _isCollectionRequest = request.IsCollection; + RequestResource = request.SecondaryResource ?? request.PrimaryResource; } protected ResourceContext GetResourceContextForScope(ResourceFieldChainExpression scope) diff --git a/src/JsonApiDotNetCore/Internal/QueryStrings/QueryStringReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs similarity index 75% rename from src/JsonApiDotNetCore/Internal/QueryStrings/QueryStringReader.cs rename to src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs index b167387b74..1716e451ca 100644 --- a/src/JsonApiDotNetCore/Internal/QueryStrings/QueryStringReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs @@ -1,14 +1,14 @@ +using System; using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.QueryStrings; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Errors; using Microsoft.Extensions.Logging; -namespace JsonApiDotNetCore.Internal.QueryStrings +namespace JsonApiDotNetCore.QueryStrings.Internal { - /// + /// public class QueryStringReader : IQueryStringReader { private readonly IJsonApiOptions _options; @@ -19,17 +19,19 @@ public class QueryStringReader : IQueryStringReader public QueryStringReader(IJsonApiOptions options, IRequestQueryStringAccessor queryStringAccessor, IEnumerable parameterReaders, ILoggerFactory loggerFactory) { - _options = options; - _queryStringAccessor = queryStringAccessor; - _parameterReaders = parameterReaders; + if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); + + _options = options ?? throw new ArgumentNullException(nameof(options)); + _queryStringAccessor = queryStringAccessor ?? throw new ArgumentNullException(nameof(queryStringAccessor)); + _parameterReaders = parameterReaders ?? throw new ArgumentNullException(nameof(parameterReaders)); _logger = loggerFactory.CreateLogger(); } - /// - public virtual void ReadAll(DisableQueryAttribute disableQueryAttribute) + /// + public virtual void ReadAll(DisableQueryStringAttribute disableQueryStringAttribute) { - disableQueryAttribute ??= DisableQueryAttribute.Empty; + disableQueryStringAttribute ??= DisableQueryStringAttribute.Empty; foreach (var (parameterName, parameterValue) in _queryStringAccessor.Query) { @@ -46,7 +48,7 @@ public virtual void ReadAll(DisableQueryAttribute disableQueryAttribute) _logger.LogDebug( $"Query string parameter '{parameterName}' with value '{parameterValue}' was accepted by {reader.GetType().Name}."); - if (!reader.IsEnabled(disableQueryAttribute)) + if (!reader.IsEnabled(disableQueryStringAttribute)) { throw new InvalidQueryStringParameterException(parameterName, "Usage of one or more query string parameters is not allowed at the requested endpoint.", diff --git a/src/JsonApiDotNetCore/Internal/QueryStrings/RequestQueryStringAccessor.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/RequestQueryStringAccessor.cs similarity index 70% rename from src/JsonApiDotNetCore/Internal/QueryStrings/RequestQueryStringAccessor.cs rename to src/JsonApiDotNetCore/QueryStrings/Internal/RequestQueryStringAccessor.cs index d221ab3f3f..5c2fd09c35 100644 --- a/src/JsonApiDotNetCore/Internal/QueryStrings/RequestQueryStringAccessor.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/RequestQueryStringAccessor.cs @@ -1,9 +1,9 @@ -using JsonApiDotNetCore.QueryStrings; +using System; using Microsoft.AspNetCore.Http; -namespace JsonApiDotNetCore.Internal.QueryStrings +namespace JsonApiDotNetCore.QueryStrings.Internal { - /// + /// internal sealed class RequestQueryStringAccessor : IRequestQueryStringAccessor { private readonly IHttpContextAccessor _httpContextAccessor; @@ -13,7 +13,7 @@ internal sealed class RequestQueryStringAccessor : IRequestQueryStringAccessor public RequestQueryStringAccessor(IHttpContextAccessor httpContextAccessor) { - _httpContextAccessor = httpContextAccessor; + _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } } } diff --git a/src/JsonApiDotNetCore/Internal/QueryStrings/ResourceDefinitionQueryableParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs similarity index 57% rename from src/JsonApiDotNetCore/Internal/QueryStrings/ResourceDefinitionQueryableParameterReader.cs rename to src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs index a50ba3a2e8..acce8a5148 100644 --- a/src/JsonApiDotNetCore/Internal/QueryStrings/ResourceDefinitionQueryableParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs @@ -1,43 +1,43 @@ +using System; using System.Collections.Generic; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.QueryStrings; -using JsonApiDotNetCore.RequestServices.Contracts; -using JsonApiDotNetCore.Services.Contract; +using JsonApiDotNetCore.Resources; using Microsoft.Extensions.Primitives; -namespace JsonApiDotNetCore.Internal.QueryStrings +namespace JsonApiDotNetCore.QueryStrings.Internal { - /// + /// public class ResourceDefinitionQueryableParameterReader : IResourceDefinitionQueryableParameterReader { - private readonly ICurrentRequest _currentRequest; + private readonly IJsonApiRequest _request; private readonly IResourceDefinitionProvider _resourceDefinitionProvider; private readonly List _constraints = new List(); - public ResourceDefinitionQueryableParameterReader(ICurrentRequest currentRequest, IResourceDefinitionProvider resourceDefinitionProvider) + public ResourceDefinitionQueryableParameterReader(IJsonApiRequest request, IResourceDefinitionProvider resourceDefinitionProvider) { - _currentRequest = currentRequest; - _resourceDefinitionProvider = resourceDefinitionProvider; + _request = request ?? throw new ArgumentNullException(nameof(request)); + _resourceDefinitionProvider = resourceDefinitionProvider ?? throw new ArgumentNullException(nameof(resourceDefinitionProvider)); } - /// - public bool IsEnabled(DisableQueryAttribute disableQueryAttribute) + /// + public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) { return true; } - /// - public bool CanRead(string parameterName) + /// + public virtual bool CanRead(string parameterName) { var queryableHandler = GetQueryableHandler(parameterName); return queryableHandler != null; } - /// - public void Read(string parameterName, StringValues parameterValue) + /// + public virtual void Read(string parameterName, StringValues parameterValue) { var queryableHandler = GetQueryableHandler(parameterName); var expressionInScope = new ExpressionInScope(null, new QueryableHandlerExpression(queryableHandler, parameterValue)); @@ -46,22 +46,22 @@ public void Read(string parameterName, StringValues parameterValue) private object GetQueryableHandler(string parameterName) { - if (_currentRequest.Kind != EndpointKind.Primary) + if (_request.Kind != EndpointKind.Primary) { throw new InvalidQueryStringParameterException(parameterName, "Custom query string parameters cannot be used on nested resource endpoints.", $"Query string parameter '{parameterName}' cannot be used on a nested resource endpoint."); } - var resourceType = _currentRequest.PrimaryResource.ResourceType; + var resourceType = _request.PrimaryResource.ResourceType; var resourceDefinition = _resourceDefinitionProvider.Get(resourceType); return resourceDefinition?.GetQueryableHandlerForQueryStringParameter(parameterName); } - /// - public IReadOnlyCollection GetConstraints() + /// + public virtual IReadOnlyCollection GetConstraints() { - return _constraints.AsReadOnly(); + return _constraints; } } } diff --git a/src/JsonApiDotNetCore/Internal/QueryStrings/SortQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs similarity index 63% rename from src/JsonApiDotNetCore/Internal/QueryStrings/SortQueryStringParameterReader.cs rename to src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs index 62c16f5e9e..514f2ba235 100644 --- a/src/JsonApiDotNetCore/Internal/QueryStrings/SortQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs @@ -1,17 +1,16 @@ +using System; using System.Collections.Generic; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Internal.Queries.Parsing; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.QueryStrings; -using JsonApiDotNetCore.RequestServices.Contracts; +using JsonApiDotNetCore.Queries.Internal.Parsing; +using JsonApiDotNetCore.Resources.Annotations; using Microsoft.Extensions.Primitives; -namespace JsonApiDotNetCore.Internal.QueryStrings +namespace JsonApiDotNetCore.QueryStrings.Internal { public class SortQueryStringParameterReader : QueryStringParameterReader, ISortQueryStringParameterReader { @@ -20,14 +19,14 @@ public class SortQueryStringParameterReader : QueryStringParameterReader, ISortQ private readonly List _constraints = new List(); private string _lastParameterName; - public SortQueryStringParameterReader(ICurrentRequest currentRequest, IResourceContextProvider resourceContextProvider) - : base(currentRequest, resourceContextProvider) + public SortQueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider) + : base(request, resourceContextProvider) { _scopeParser = new QueryStringParameterScopeParser(resourceContextProvider, FieldChainRequirements.EndsInToMany); _sortParser = new SortParser(resourceContextProvider, ValidateSingleField); } - private void ValidateSingleField(ResourceFieldAttribute field, ResourceContext resourceContext, string path) + protected void ValidateSingleField(ResourceFieldAttribute field, ResourceContext resourceContext, string path) { if (field is AttrAttribute attribute && !attribute.Capabilities.HasFlag(AttrCapabilities.AllowSort)) { @@ -36,21 +35,23 @@ private void ValidateSingleField(ResourceFieldAttribute field, ResourceContext r } } - /// - public bool IsEnabled(DisableQueryAttribute disableQueryAttribute) + /// + public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) { - return !disableQueryAttribute.ContainsParameter(StandardQueryStringParameters.Sort); + if (disableQueryStringAttribute == null) throw new ArgumentNullException(nameof(disableQueryStringAttribute)); + + return !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Sort); } - /// - public bool CanRead(string parameterName) + /// + public virtual bool CanRead(string parameterName) { - var isNested = parameterName.StartsWith("sort[") && parameterName.EndsWith("]"); + var isNested = parameterName.StartsWith("sort[", StringComparison.Ordinal) && parameterName.EndsWith("]", StringComparison.Ordinal); return parameterName == "sort" || isNested; } - /// - public void Read(string parameterName, StringValues parameterValue) + /// + public virtual void Read(string parameterName, StringValues parameterValue) { _lastParameterName = parameterName; @@ -86,10 +87,10 @@ private SortExpression GetSort(string parameterValue, ResourceFieldChainExpressi return _sortParser.Parse(parameterValue, resourceContextInScope); } - /// - public IReadOnlyCollection GetConstraints() + /// + public virtual IReadOnlyCollection GetConstraints() { - return _constraints.AsReadOnly(); + return _constraints; } } } diff --git a/src/JsonApiDotNetCore/Internal/QueryStrings/SparseFieldSetQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs similarity index 63% rename from src/JsonApiDotNetCore/Internal/QueryStrings/SparseFieldSetQueryStringParameterReader.cs rename to src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs index 91f005c93e..686c890d3c 100644 --- a/src/JsonApiDotNetCore/Internal/QueryStrings/SparseFieldSetQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs @@ -1,17 +1,16 @@ +using System; using System.Collections.Generic; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Internal.Queries.Parsing; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.QueryStrings; -using JsonApiDotNetCore.RequestServices.Contracts; +using JsonApiDotNetCore.Queries.Internal.Parsing; +using JsonApiDotNetCore.Resources.Annotations; using Microsoft.Extensions.Primitives; -namespace JsonApiDotNetCore.Internal.QueryStrings +namespace JsonApiDotNetCore.QueryStrings.Internal { public class SparseFieldSetQueryStringParameterReader : QueryStringParameterReader, ISparseFieldSetQueryStringParameterReader { @@ -20,14 +19,14 @@ public class SparseFieldSetQueryStringParameterReader : QueryStringParameterRead private readonly List _constraints = new List(); private string _lastParameterName; - public SparseFieldSetQueryStringParameterReader(ICurrentRequest currentRequest, IResourceContextProvider resourceContextProvider) - : base(currentRequest, resourceContextProvider) + public SparseFieldSetQueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider) + : base(request, resourceContextProvider) { _sparseFieldSetParser = new SparseFieldSetParser(resourceContextProvider, ValidateSingleAttribute); _scopeParser = new QueryStringParameterScopeParser(resourceContextProvider, FieldChainRequirements.IsRelationship); } - private void ValidateSingleAttribute(AttrAttribute attribute, ResourceContext resourceContext, string path) + protected void ValidateSingleAttribute(AttrAttribute attribute, ResourceContext resourceContext, string path) { if (!attribute.Capabilities.HasFlag(AttrCapabilities.AllowView)) { @@ -36,21 +35,23 @@ private void ValidateSingleAttribute(AttrAttribute attribute, ResourceContext re } } - /// - public bool IsEnabled(DisableQueryAttribute disableQueryAttribute) + /// + public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) { - return !disableQueryAttribute.ContainsParameter(StandardQueryStringParameters.Fields); + if (disableQueryStringAttribute == null) throw new ArgumentNullException(nameof(disableQueryStringAttribute)); + + return !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Fields); } - /// - public bool CanRead(string parameterName) + /// + public virtual bool CanRead(string parameterName) { - var isNested = parameterName.StartsWith("fields[") && parameterName.EndsWith("]"); + var isNested = parameterName.StartsWith("fields[", StringComparison.Ordinal) && parameterName.EndsWith("]", StringComparison.Ordinal); return parameterName == "fields" || isNested; } - /// - public void Read(string parameterName, StringValues parameterValue) + /// + public virtual void Read(string parameterName, StringValues parameterValue) { _lastParameterName = parameterName; @@ -81,10 +82,10 @@ private SparseFieldSetExpression GetSparseFieldSet(string parameterValue, Resour return _sparseFieldSetParser.Parse(parameterValue, resourceContextInScope); } - /// - public IReadOnlyCollection GetConstraints() + /// + public virtual IReadOnlyCollection GetConstraints() { - return _constraints.AsReadOnly(); + return _constraints; } } } diff --git a/src/JsonApiDotNetCore/Controllers/StandardQueryStringParameters.cs b/src/JsonApiDotNetCore/QueryStrings/StandardQueryStringParameters.cs similarity index 59% rename from src/JsonApiDotNetCore/Controllers/StandardQueryStringParameters.cs rename to src/JsonApiDotNetCore/QueryStrings/StandardQueryStringParameters.cs index 766b564d54..f488e823f6 100644 --- a/src/JsonApiDotNetCore/Controllers/StandardQueryStringParameters.cs +++ b/src/JsonApiDotNetCore/QueryStrings/StandardQueryStringParameters.cs @@ -1,7 +1,11 @@ using System; +using JsonApiDotNetCore.Controllers.Annotations; -namespace JsonApiDotNetCore.Controllers +namespace JsonApiDotNetCore.QueryStrings { + /// + /// Lists query string parameters used by . + /// [Flags] public enum StandardQueryStringParameters { diff --git a/src/JsonApiDotNetCore/Repositories/DbContextExtensions.cs b/src/JsonApiDotNetCore/Repositories/DbContextExtensions.cs new file mode 100644 index 0000000000..ebc1ec6498 --- /dev/null +++ b/src/JsonApiDotNetCore/Repositories/DbContextExtensions.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using JsonApiDotNetCore.Resources; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; + +namespace JsonApiDotNetCore.Repositories +{ + public static class DbContextExtensions + { + /// + /// Determines whether or not EF is already tracking an entity of the same Type and Id + /// and returns that entity. + /// + internal static TEntity GetTrackedEntity(this DbContext context, TEntity entity) + where TEntity : class, IIdentifiable + { + if (entity == null) + throw new ArgumentNullException(nameof(entity)); + + var entityEntry = context.ChangeTracker + .Entries() + .FirstOrDefault(entry => + entry.Entity.GetType() == entity.GetType() && + ((IIdentifiable) entry.Entity).StringId == entity.StringId); + + return (TEntity) entityEntry?.Entity; + } + + /// + /// Gets the current transaction or creates a new one. + /// If a transaction already exists, commit, rollback and dispose + /// will not be called. It is assumed the creator of the original + /// transaction should be responsible for disposal. + /// + /// + /// + /// + /// using(var transaction = _context.GetCurrentOrCreateTransaction()) + /// { + /// // perform multiple operations on the context and then save... + /// _context.SaveChanges(); + /// } + /// + /// + public static async Task GetCurrentOrCreateTransactionAsync(this DbContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + + return await SafeTransactionProxy.GetOrCreateAsync(context.Database); + } + } +} diff --git a/src/JsonApiDotNetCore/Data/DbContextResolver.cs b/src/JsonApiDotNetCore/Repositories/DbContextResolver.cs similarity index 67% rename from src/JsonApiDotNetCore/Data/DbContextResolver.cs rename to src/JsonApiDotNetCore/Repositories/DbContextResolver.cs index a485ae22be..f0f6ea166f 100644 --- a/src/JsonApiDotNetCore/Data/DbContextResolver.cs +++ b/src/JsonApiDotNetCore/Repositories/DbContextResolver.cs @@ -1,7 +1,9 @@ +using System; using Microsoft.EntityFrameworkCore; -namespace JsonApiDotNetCore.Data +namespace JsonApiDotNetCore.Repositories { + /// public sealed class DbContextResolver : IDbContextResolver where TDbContext : DbContext { @@ -9,7 +11,7 @@ public sealed class DbContextResolver : IDbContextResolver public DbContextResolver(TDbContext context) { - _context = context; + _context = context ?? throw new ArgumentNullException(nameof(context)); } public DbContext GetContext() => _context; diff --git a/src/JsonApiDotNetCore/Data/EntityFrameworkCoreRepository.cs b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs similarity index 79% rename from src/JsonApiDotNetCore/Data/EntityFrameworkCoreRepository.cs rename to src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs index 406815c453..e7c081efa3 100644 --- a/src/JsonApiDotNetCore/Data/EntityFrameworkCoreRepository.cs +++ b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs @@ -3,23 +3,20 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Internal.Generics; -using JsonApiDotNetCore.Internal.Queries.QueryableBuilding; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Queries.Internal.QueryableBuilding; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace JsonApiDotNetCore.Data +namespace JsonApiDotNetCore.Repositories { /// - /// Provides a repository implementation that uses Entity Framework Core. + /// Implements the foundational Repository layer in the JsonApiDotNetCore architecture that uses Entity Framework Core. /// public class EntityFrameworkCoreRepository : IResourceRepository where TResource : class, IIdentifiable @@ -30,7 +27,7 @@ public class EntityFrameworkCoreRepository : IResourceRepository private readonly IGenericServiceFactory _genericServiceFactory; private readonly IResourceFactory _resourceFactory; private readonly IEnumerable _constraintProviders; - private readonly ILogger> _logger; + private readonly TraceLogWriter> _traceWriter; public EntityFrameworkCoreRepository( ITargetedFields targetedFields, @@ -41,24 +38,23 @@ public EntityFrameworkCoreRepository( IEnumerable constraintProviders, ILoggerFactory loggerFactory) { - _targetedFields = targetedFields; - _resourceGraph = resourceGraph; - _genericServiceFactory = genericServiceFactory; - _resourceFactory = resourceFactory; - _constraintProviders = constraintProviders; + if (contextResolver == null) throw new ArgumentNullException(nameof(contextResolver)); + if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); + + _targetedFields = targetedFields ?? throw new ArgumentNullException(nameof(targetedFields)); + _resourceGraph = resourceGraph ?? throw new ArgumentNullException(nameof(resourceGraph)); + _genericServiceFactory = genericServiceFactory ?? throw new ArgumentNullException(nameof(genericServiceFactory)); + _resourceFactory = resourceFactory ?? throw new ArgumentNullException(nameof(resourceFactory)); + _constraintProviders = constraintProviders ?? throw new ArgumentNullException(nameof(constraintProviders)); _dbContext = contextResolver.GetContext(); - _logger = loggerFactory.CreateLogger>(); + _traceWriter = new TraceLogWriter>(loggerFactory); } /// public virtual async Task> GetAsync(QueryLayer layer) { - _logger.LogTrace($"Entering {nameof(GetAsync)}('{layer}')."); - - if (layer == null) - { - throw new ArgumentNullException(nameof(layer)); - } + _traceWriter.LogMethodStart(new {layer}); + if (layer == null) throw new ArgumentNullException(nameof(layer)); IQueryable query = ApplyQueryLayer(layer); return await query.ToListAsync(); @@ -67,7 +63,7 @@ public virtual async Task> GetAsync(QueryLayer la /// public virtual async Task CountAsync(FilterExpression topFilter) { - _logger.LogTrace($"Entering {nameof(CountAsync)}('{topFilter}')."); + _traceWriter.LogMethodStart(new {topFilter}); var resourceContext = _resourceGraph.GetResourceContext(); var layer = new QueryLayer(resourceContext) @@ -81,6 +77,9 @@ public virtual async Task CountAsync(FilterExpression topFilter) protected virtual IQueryable ApplyQueryLayer(QueryLayer layer) { + _traceWriter.LogMethodStart(new {layer}); + if (layer == null) throw new ArgumentNullException(nameof(layer)); + IQueryable source = GetAll(); var queryableHandlers = _constraintProviders @@ -88,7 +87,7 @@ protected virtual IQueryable ApplyQueryLayer(QueryLayer layer) .Where(expressionInScope => expressionInScope.Scope == null) .Select(expressionInScope => expressionInScope.Expression) .OfType() - .ToList(); + .ToArray(); foreach (var queryableHandler in queryableHandlers) { @@ -110,7 +109,8 @@ protected virtual IQueryable GetAll() /// public virtual async Task CreateAsync(TResource resource) { - _logger.LogTrace($"Entering {nameof(CreateAsync)}({(resource == null ? "null" : "object")})."); + _traceWriter.LogMethodStart(new {resource}); + if (resource == null) throw new ArgumentNullException(nameof(resource)); foreach (var relationshipAttr in _targetedFields.Relationships) { @@ -141,8 +141,8 @@ public virtual async Task CreateAsync(TResource resource) /// /// Consider the following example: /// person.todoItems = [t1,t2] is updated to [t3, t4]. If t3, and/or t4 was - /// already related to a other person, and these persons are NOT loaded in to the - /// db context, then the query may cause a foreign key constraint. Loading + /// already related to a other person, and these persons are NOT loaded into the + /// DbContext, then the query may cause a foreign key constraint. Loading /// these "inverse relationships" into the DB context ensures EF core to take /// this into account. /// @@ -177,7 +177,7 @@ private bool IsHasOneRelationship(string internalRelationshipName, Type type) } // relationshipAttr is null when we don't put a [RelationshipAttribute] on the inverse navigation property. // In this case we use reflection to figure out what kind of relationship is pointing back. - return !type.GetProperty(internalRelationshipName).PropertyType.IsOrImplementsInterface(typeof(IEnumerable)); + return !TypeHelper.IsOrImplementsInterface(type.GetProperty(internalRelationshipName).PropertyType, typeof(IEnumerable)); } private void DetachRelationships(TResource resource) @@ -208,7 +208,9 @@ private void DetachRelationships(TResource resource) /// public virtual async Task UpdateAsync(TResource requestResource, TResource databaseResource) { - _logger.LogTrace($"Entering {nameof(UpdateAsync)}({(requestResource == null ? "null" : "object")}, {(databaseResource == null ? "null" : "object")})."); + _traceWriter.LogMethodStart(new {requestResource, databaseResource}); + if (requestResource == null) throw new ArgumentNullException(nameof(requestResource)); + if (databaseResource == null) throw new ArgumentNullException(nameof(databaseResource)); foreach (var attribute in _targetedFields.Attributes) attribute.SetValue(databaseResource, attribute.GetValue(requestResource)); @@ -250,26 +252,25 @@ private object GetTrackedRelationshipValue(RelationshipAttribute relationshipAtt return GetTrackedHasOneRelationshipValue(relationshipValue, ref wasAlreadyAttached); } - IEnumerable relationshipValueList = (IEnumerable)relationshipAttr.GetValue(resource); - if (relationshipValueList == null) + IEnumerable relationshipValues = (IEnumerable)relationshipAttr.GetValue(resource); + if (relationshipValues == null) return null; - return GetTrackedManyRelationshipValue(relationshipValueList, relationshipAttr, ref wasAlreadyAttached); + return GetTrackedManyRelationshipValue(relationshipValues, relationshipAttr, ref wasAlreadyAttached); } // helper method used in GetTrackedRelationshipValue. See comments below. - private IEnumerable GetTrackedManyRelationshipValue(IEnumerable relationshipValueList, RelationshipAttribute relationshipAttr, ref bool wasAlreadyAttached) + private IEnumerable GetTrackedManyRelationshipValue(IEnumerable relationshipValues, RelationshipAttribute relationshipAttr, ref bool wasAlreadyAttached) { - if (relationshipValueList == null) return null; + if (relationshipValues == null) return null; bool newWasAlreadyAttached = false; - var trackedPointerCollection = relationshipValueList.Select(pointer => - { - // convert each element in the value list to relationshipAttr.DependentType. - var tracked = AttachOrGetTracked(pointer); - if (tracked != null) newWasAlreadyAttached = true; - return Convert.ChangeType(tracked ?? pointer, relationshipAttr.RightType); - }) - .CopyToTypedCollection(relationshipAttr.Property.PropertyType); + var trackedPointerCollection = TypeHelper.CopyToTypedCollection(relationshipValues.Select(pointer => + { + // convert each element in the value list to relationshipAttr.DependentType. + var tracked = AttachOrGetTracked(pointer); + if (tracked != null) newWasAlreadyAttached = true; + return Convert.ChangeType(tracked ?? pointer, relationshipAttr.RightType); + }), relationshipAttr.Property.PropertyType); if (newWasAlreadyAttached) wasAlreadyAttached = true; return trackedPointerCollection; @@ -284,11 +285,14 @@ private IIdentifiable GetTrackedHasOneRelationshipValue(IIdentifiable relationsh } /// - public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds) + public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IReadOnlyCollection relationshipIds) { - _logger.LogTrace($"Entering {nameof(UpdateRelationshipsAsync)}({nameof(parent)}, {nameof(relationship)}, {nameof(relationshipIds)})."); + _traceWriter.LogMethodStart(new {parent, relationship, relationshipIds}); + if (parent == null) throw new ArgumentNullException(nameof(parent)); + if (relationship == null) throw new ArgumentNullException(nameof(relationship)); + if (relationshipIds == null) throw new ArgumentNullException(nameof(relationshipIds)); - var typeToUpdate = (relationship is HasManyThroughAttribute hasManyThrough) + var typeToUpdate = relationship is HasManyThroughAttribute hasManyThrough ? hasManyThrough.ThroughType : relationship.RightType; @@ -301,7 +305,7 @@ public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute /// public virtual async Task DeleteAsync(TId id) { - _logger.LogTrace($"Entering {nameof(DeleteAsync)}('{id}')."); + _traceWriter.LogMethodStart(new {id}); var resourceToDelete = _resourceFactory.CreateInstance(); resourceToDelete.Id = id; @@ -329,9 +333,11 @@ public virtual async Task DeleteAsync(TId id) } } + /// public virtual void FlushFromCache(TResource resource) { - _logger.LogTrace($"Entering {nameof(FlushFromCache)}({nameof(resource)})."); + _traceWriter.LogMethodStart(new {resource}); + if (resource == null) throw new ArgumentNullException(nameof(resource)); _dbContext.Entry(resource).State = EntityState.Detached; } @@ -351,6 +357,9 @@ public virtual void FlushFromCache(TResource resource) /// protected void LoadCurrentRelationships(TResource oldResource, RelationshipAttribute relationshipAttribute) { + if (oldResource == null) throw new ArgumentNullException(nameof(oldResource)); + if (relationshipAttribute == null) throw new ArgumentNullException(nameof(relationshipAttribute)); + if (relationshipAttribute is HasManyThroughAttribute throughAttribute) { _dbContext.Entry(oldResource).Collection(throughAttribute.ThroughProperty.Name).Load(); @@ -391,7 +400,7 @@ private IIdentifiable AttachOrGetTracked(IIdentifiable relationshipValue) } /// - /// Provides a repository implementation that uses Entity Framework Core. + /// Implements the foundational repository implementation that uses Entity Framework Core. /// public class EntityFrameworkCoreRepository : EntityFrameworkCoreRepository, IResourceRepository where TResource : class, IIdentifiable diff --git a/src/JsonApiDotNetCore/Repositories/IDbContextResolver.cs b/src/JsonApiDotNetCore/Repositories/IDbContextResolver.cs new file mode 100644 index 0000000000..693724ed90 --- /dev/null +++ b/src/JsonApiDotNetCore/Repositories/IDbContextResolver.cs @@ -0,0 +1,12 @@ +using Microsoft.EntityFrameworkCore; + +namespace JsonApiDotNetCore.Repositories +{ + /// + /// Provides a method to resolve a . + /// + public interface IDbContextResolver + { + DbContext GetContext(); + } +} diff --git a/src/JsonApiDotNetCore/Repositories/IRepositoryRelationshipUpdateHelper.cs b/src/JsonApiDotNetCore/Repositories/IRepositoryRelationshipUpdateHelper.cs new file mode 100644 index 0000000000..376644cb2e --- /dev/null +++ b/src/JsonApiDotNetCore/Repositories/IRepositoryRelationshipUpdateHelper.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCore.Repositories +{ + /// + /// A special helper that processes updates of relationships + /// + /// + /// This service required to be able translate involved expressions into queries + /// instead of having them evaluated on the client side. In particular, for all three types of relationship + /// a lookup is performed based on an ID. Expressions that use IIdentifiable.StringId can never + /// be translated into queries because this property only exists at runtime after the query is performed. + /// We will have to build expression trees if we want to use IIdentifiable{TId}.TId, for which we minimally a + /// generic execution to DbContext.Set{T}(). + /// + public interface IRepositoryRelationshipUpdateHelper + { + /// + /// Processes updates of relationships + /// + Task UpdateRelationshipAsync(IIdentifiable parent, RelationshipAttribute relationship, IReadOnlyCollection relationshipIds); + } +} diff --git a/src/JsonApiDotNetCore/Data/IResourceReadRepository.cs b/src/JsonApiDotNetCore/Repositories/IResourceReadRepository.cs similarity index 70% rename from src/JsonApiDotNetCore/Data/IResourceReadRepository.cs rename to src/JsonApiDotNetCore/Repositories/IResourceReadRepository.cs index 81b5c25b3a..9c04f4bcc5 100644 --- a/src/JsonApiDotNetCore/Data/IResourceReadRepository.cs +++ b/src/JsonApiDotNetCore/Repositories/IResourceReadRepository.cs @@ -1,21 +1,27 @@ using System.Collections.Generic; using System.Threading.Tasks; -using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources; -namespace JsonApiDotNetCore.Data +namespace JsonApiDotNetCore.Repositories { + /// public interface IResourceReadRepository : IResourceReadRepository where TResource : class, IIdentifiable { } + /// + /// Groups read operations. + /// + /// The resource type. + /// The resource identifier type. public interface IResourceReadRepository where TResource : class, IIdentifiable { /// - /// Executes a read query using the specified constraints and returns the list of matching resources. + /// Executes a read query using the specified constraints and returns the collection of matching resources. /// Task> GetAsync(QueryLayer layer); diff --git a/src/JsonApiDotNetCore/Repositories/IResourceRepository.cs b/src/JsonApiDotNetCore/Repositories/IResourceRepository.cs new file mode 100644 index 0000000000..81efe34e54 --- /dev/null +++ b/src/JsonApiDotNetCore/Repositories/IResourceRepository.cs @@ -0,0 +1,21 @@ +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Repositories +{ + /// + public interface IResourceRepository + : IResourceRepository + where TResource : class, IIdentifiable + { } + + /// + /// Represents the foundational Resource Repository layer in the JsonApiDotNetCore architecture that provides data access to an underlying store. + /// + /// The resource type. + /// The resource identifier type. + public interface IResourceRepository + : IResourceReadRepository, + IResourceWriteRepository + where TResource : class, IIdentifiable + { } +} diff --git a/src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs b/src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs new file mode 100644 index 0000000000..301ba6ef7f --- /dev/null +++ b/src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCore.Repositories +{ + /// + public interface IResourceWriteRepository + : IResourceWriteRepository + where TResource : class, IIdentifiable + { } + + /// + /// Groups write operations. + /// + /// The resource type. + /// The resource identifier type. + public interface IResourceWriteRepository + where TResource : class, IIdentifiable + { + /// + /// Creates a new resource in the underlying data store. + /// + Task CreateAsync(TResource resource); + + /// + /// Updates an existing resource in the underlying data store. + /// + /// The (partial) resource coming from the request body. + /// The resource as stored in the database before the update. + Task UpdateAsync(TResource requestResource, TResource databaseResource); + + /// + /// Updates relationships in the underlying data store. + /// + Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IReadOnlyCollection relationshipIds); + + /// + /// Deletes a resource from the underlying data store. + /// + /// Identifier for the resource to delete. + /// true if the resource was deleted; false is the resource did not exist. + Task DeleteAsync(TId id); + + /// + /// Ensures that the next time this resource is requested, it is re-fetched from the underlying data store. + /// + void FlushFromCache(TResource resource); + } +} diff --git a/src/JsonApiDotNetCore/Internal/Generics/RepositoryRelationshipUpdateHelper.cs b/src/JsonApiDotNetCore/Repositories/RepositoryRelationshipUpdateHelper.cs similarity index 73% rename from src/JsonApiDotNetCore/Internal/Generics/RepositoryRelationshipUpdateHelper.cs rename to src/JsonApiDotNetCore/Repositories/RepositoryRelationshipUpdateHelper.cs index 58b25e58dd..d349abe17c 100644 --- a/src/JsonApiDotNetCore/Internal/Generics/RepositoryRelationshipUpdateHelper.cs +++ b/src/JsonApiDotNetCore/Repositories/RepositoryRelationshipUpdateHelper.cs @@ -4,34 +4,13 @@ using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; using Microsoft.EntityFrameworkCore; -namespace JsonApiDotNetCore.Internal.Generics +namespace JsonApiDotNetCore.Repositories { - /// - /// A special helper that processes updates of relationships - /// - /// - /// This service required to be able translate involved expressions into queries - /// instead of having them evaluated on the client side. In particular, for all three types of relationship - /// a lookup is performed based on an id. Expressions that use IIdentifiable.StringId can never - /// be translated into queries because this property only exists at runtime after the query is performed. - /// We will have to build expression trees if we want to use IIdentifiable{TId}.TId, for which we minimally a - /// generic execution to DbContext.Set{T}(). - /// - public interface IRepositoryRelationshipUpdateHelper - { - /// - /// Processes updates of relationships - /// - Task UpdateRelationshipAsync(IIdentifiable parent, RelationshipAttribute relationship, IEnumerable relationshipIds); - } - - /// + /// public class RepositoryRelationshipUpdateHelper : IRepositoryRelationshipUpdateHelper where TRelatedResource : class { private readonly IResourceFactory _resourceFactory; @@ -39,13 +18,19 @@ public class RepositoryRelationshipUpdateHelper : IRepositoryR public RepositoryRelationshipUpdateHelper(IDbContextResolver contextResolver, IResourceFactory resourceFactory) { - _resourceFactory = resourceFactory; + if (contextResolver == null) throw new ArgumentNullException(nameof(contextResolver)); + + _resourceFactory = resourceFactory ?? throw new ArgumentNullException(nameof(resourceFactory)); _context = contextResolver.GetContext(); } - /// - public virtual async Task UpdateRelationshipAsync(IIdentifiable parent, RelationshipAttribute relationship, IEnumerable relationshipIds) + /// + public virtual async Task UpdateRelationshipAsync(IIdentifiable parent, RelationshipAttribute relationship, IReadOnlyCollection relationshipIds) { + if (parent == null) throw new ArgumentNullException(nameof(parent)); + if (relationship == null) throw new ArgumentNullException(nameof(relationship)); + if (relationshipIds == null) throw new ArgumentNullException(nameof(relationshipIds)); + if (relationship is HasManyThroughAttribute hasManyThrough) await UpdateManyToManyAsync(parent, hasManyThrough, relationshipIds); else if (relationship is HasManyAttribute) @@ -54,7 +39,7 @@ public virtual async Task UpdateRelationshipAsync(IIdentifiable parent, Relation await UpdateOneToOneAsync(parent, relationship, relationshipIds); } - private async Task UpdateOneToOneAsync(IIdentifiable parent, RelationshipAttribute relationship, IEnumerable relationshipIds) + private async Task UpdateOneToOneAsync(IIdentifiable parent, RelationshipAttribute relationship, IReadOnlyCollection relationshipIds) { TRelatedResource value = null; if (relationshipIds.Any()) @@ -72,18 +57,18 @@ private async Task UpdateOneToOneAsync(IIdentifiable parent, RelationshipAttribu relationship.SetValue(parent, value, _resourceFactory); } - private async Task UpdateOneToManyAsync(IIdentifiable parent, RelationshipAttribute relationship, IEnumerable relationshipIds) + private async Task UpdateOneToManyAsync(IIdentifiable parent, RelationshipAttribute relationship, IReadOnlyCollection relationshipIds) { IEnumerable value; if (!relationshipIds.Any()) { - var collectionType = relationship.Property.PropertyType.ToConcreteCollectionType(); + var collectionType = TypeHelper.ToConcreteCollectionType(relationship.Property.PropertyType); value = (IEnumerable)TypeHelper.CreateInstance(collectionType); } else { var idType = TypeHelper.GetIdType(relationship.RightType); - var typedIds = relationshipIds.CopyToList(idType, stringId => TypeHelper.ConvertType(stringId, idType)); + var typedIds = TypeHelper.CopyToList(relationshipIds, idType, stringId => TypeHelper.ConvertType(stringId, idType)); // [1, 2, 3] var target = Expression.Constant(typedIds); @@ -96,13 +81,13 @@ private async Task UpdateOneToManyAsync(IIdentifiable parent, RelationshipAttrib var containsLambda = Expression.Lambda>(callContains, parameter); var resultSet = await _context.Set().Where(containsLambda).ToListAsync(); - value = resultSet.CopyToTypedCollection(relationship.Property.PropertyType); + value = TypeHelper.CopyToTypedCollection(resultSet, relationship.Property.PropertyType); } relationship.SetValue(parent, value, _resourceFactory); } - private async Task UpdateManyToManyAsync(IIdentifiable parent, HasManyThroughAttribute relationship, IEnumerable relationshipIds) + private async Task UpdateManyToManyAsync(IIdentifiable parent, HasManyThroughAttribute relationship, IReadOnlyCollection relationshipIds) { // we need to create a transaction for the HasManyThrough case so we can get and remove any existing // through resources and only commit if all operations are successful @@ -137,7 +122,7 @@ private async Task UpdateManyToManyAsync(IIdentifiable parent, HasManyThroughAtt _context.AddRange(newLinks); await _context.SaveChangesAsync(); - transaction.Commit(); + await transaction.CommitAsync(); } } } diff --git a/src/JsonApiDotNetCore/Repositories/SafeTransactionProxy.cs b/src/JsonApiDotNetCore/Repositories/SafeTransactionProxy.cs new file mode 100644 index 0000000000..36c1e39b40 --- /dev/null +++ b/src/JsonApiDotNetCore/Repositories/SafeTransactionProxy.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; + +namespace JsonApiDotNetCore.Repositories +{ + /// + /// Gets the current transaction or creates a new one. + /// If a transaction already exists, commit, rollback and dispose + /// will not be called. It is assumed the creator of the original + /// transaction should be responsible for disposal. + /// + internal class SafeTransactionProxy : IDbContextTransaction + { + private readonly bool _shouldExecute; + private readonly IDbContextTransaction _transaction; + + private SafeTransactionProxy(IDbContextTransaction transaction, bool shouldExecute) + { + _transaction = transaction ?? throw new ArgumentNullException(nameof(transaction)); + _shouldExecute = shouldExecute; + } + + public static async Task GetOrCreateAsync(DatabaseFacade databaseFacade) + { + if (databaseFacade == null) throw new ArgumentNullException(nameof(databaseFacade)); + + return databaseFacade.CurrentTransaction != null + ? new SafeTransactionProxy(databaseFacade.CurrentTransaction, shouldExecute: false) + : new SafeTransactionProxy(await databaseFacade.BeginTransactionAsync(), shouldExecute: true); + } + + /// + public Guid TransactionId => _transaction.TransactionId; + + /// + public void Commit() => Proxy(t => t.Commit()); + + /// + public void Rollback() => Proxy(t => t.Rollback()); + + /// + public void Dispose() => Proxy(t => t.Dispose()); + + private void Proxy(Action action) + { + if(_shouldExecute) + action(_transaction); + } + + public Task CommitAsync(CancellationToken cancellationToken = default) + { + return _transaction.CommitAsync(cancellationToken); + } + + public Task RollbackAsync(CancellationToken cancellationToken = default) + { + return _transaction.RollbackAsync(cancellationToken); + } + + public ValueTask DisposeAsync() + { + return _transaction.DisposeAsync(); + } + } +} diff --git a/src/JsonApiDotNetCore/RequestServices/Contracts/ITargetedFields.cs b/src/JsonApiDotNetCore/RequestServices/Contracts/ITargetedFields.cs deleted file mode 100644 index f00e4386f2..0000000000 --- a/src/JsonApiDotNetCore/RequestServices/Contracts/ITargetedFields.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Models.Annotation; - -namespace JsonApiDotNetCore.Serialization -{ - /// - /// Container to register which attributes and relationships are targeted by the current operation. - /// - public interface ITargetedFields - { - /// - /// List of attributes that are updated by a request - /// - List Attributes { get; set; } - /// - /// List of relationships that are updated by a request - /// - List Relationships { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/RequestServices/CurrentRequest.cs b/src/JsonApiDotNetCore/RequestServices/CurrentRequest.cs deleted file mode 100644 index 9c35d485b9..0000000000 --- a/src/JsonApiDotNetCore/RequestServices/CurrentRequest.cs +++ /dev/null @@ -1,18 +0,0 @@ -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.RequestServices.Contracts; - -namespace JsonApiDotNetCore.RequestServices -{ - public sealed class CurrentRequest : ICurrentRequest - { - public EndpointKind Kind { get; set; } - public string BasePath { get; set; } - public string PrimaryId { get; set; } - public ResourceContext PrimaryResource { get; set; } - public ResourceContext SecondaryResource { get; set; } - public RelationshipAttribute Relationship { get; set; } - public bool IsCollection { get; set; } - public bool IsReadOnly { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/RequestServices/TargetedFields.cs b/src/JsonApiDotNetCore/RequestServices/TargetedFields.cs deleted file mode 100644 index 3ba1db54eb..0000000000 --- a/src/JsonApiDotNetCore/RequestServices/TargetedFields.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Models.Annotation; - -namespace JsonApiDotNetCore.Serialization -{ - /// - public sealed class TargetedFields : ITargetedFields - { - /// - public List Attributes { get; set; } = new List(); - /// - public List Relationships { get; set; } = new List(); - } - -} diff --git a/src/JsonApiDotNetCore/Resources/Annotations/AttrAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/AttrAttribute.cs new file mode 100644 index 0000000000..4f379bcab8 --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/Annotations/AttrAttribute.cs @@ -0,0 +1,74 @@ +using System; + +namespace JsonApiDotNetCore.Resources.Annotations +{ + /// + /// Used to expose a property on a resource class as a json:api attribute (https://jsonapi.org/format/#document-resource-object-attributes). + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class AttrAttribute : ResourceFieldAttribute + { + private AttrCapabilities? _capabilities; + + internal bool HasExplicitCapabilities => _capabilities != null; + + /// + /// The set of capabilities that are allowed to be performed on this attribute. + /// When not explicitly assigned, the configured default set of capabilities is used. + /// + /// + /// + /// public class Author : Identifiable + /// { + /// [Attr(Capabilities = AttrCapabilities.AllowFilter | AttrCapabilities.AllowSort)] + /// public string Name { get; set; } + /// } + /// + /// + public AttrCapabilities Capabilities + { + get => _capabilities ?? default; + set => _capabilities = value; + } + + /// + /// Get the value of the attribute for the given object. + /// Returns null if the attribute does not belong to the + /// provided object. + /// + public object GetValue(object resource) + { + if (resource == null) + { + throw new ArgumentNullException(nameof(resource)); + } + + if (Property.GetMethod == null) + { + throw new InvalidOperationException($"Property '{Property.DeclaringType?.Name}.{Property.Name}' is write-only."); + } + + return Property.GetValue(resource); + } + + /// + /// Sets the value of the attribute on the given object. + /// + public void SetValue(object resource, object newValue) + { + if (resource == null) + { + throw new ArgumentNullException(nameof(resource)); + } + + if (Property.SetMethod == null) + { + throw new InvalidOperationException( + $"Property '{Property.DeclaringType?.Name}.{Property.Name}' is read-only."); + } + + var convertedValue = TypeHelper.ConvertType(newValue, Property.PropertyType); + Property.SetValue(resource, convertedValue); + } + } +} diff --git a/src/JsonApiDotNetCore/Models/AttrCapabilities.cs b/src/JsonApiDotNetCore/Resources/Annotations/AttrCapabilities.cs similarity index 84% rename from src/JsonApiDotNetCore/Models/AttrCapabilities.cs rename to src/JsonApiDotNetCore/Resources/Annotations/AttrCapabilities.cs index c37fa6a681..7205f01241 100644 --- a/src/JsonApiDotNetCore/Models/AttrCapabilities.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/AttrCapabilities.cs @@ -1,16 +1,15 @@ using System; -using JsonApiDotNetCore.Models.Annotation; -namespace JsonApiDotNetCore.Models +namespace JsonApiDotNetCore.Resources.Annotations { /// - /// Indicates query string capabilities that can be performed on an . + /// Indicates capabilities that can be performed on an . /// [Flags] public enum AttrCapabilities { None = 0, - + /// /// Whether or not GET requests can return the attribute. /// Attempts to retrieve when disabled will return an HTTP 422 response. @@ -22,13 +21,13 @@ public enum AttrCapabilities /// Attempts to update when disabled will return an HTTP 422 response. /// AllowChange = 2, - + /// /// Whether or not an attribute can be filtered on via a query string parameter. /// Attempts to sort when disabled will return an HTTP 400 response. /// AllowFilter = 4, - + /// /// Whether or not an attribute can be sorted on via a query string parameter. /// Attempts to sort when disabled will return an HTTP 400 response. diff --git a/src/JsonApiDotNetCore/Models/Annotation/EagerLoadAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/EagerLoadAttribute.cs similarity index 90% rename from src/JsonApiDotNetCore/Models/Annotation/EagerLoadAttribute.cs rename to src/JsonApiDotNetCore/Resources/Annotations/EagerLoadAttribute.cs index 78a599757a..be5adc2339 100644 --- a/src/JsonApiDotNetCore/Models/Annotation/EagerLoadAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/EagerLoadAttribute.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Reflection; -namespace JsonApiDotNetCore.Models.Annotation +namespace JsonApiDotNetCore.Resources.Annotations { /// /// Used to unconditionally load a related entity that is not exposed as a json:api relationship. @@ -38,6 +38,6 @@ public sealed class EagerLoadAttribute : Attribute { public PropertyInfo Property { get; internal set; } - public IList Children { get; internal set; } + public IReadOnlyCollection Children { get; internal set; } } } diff --git a/src/JsonApiDotNetCore/Resources/Annotations/HasManyAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/HasManyAttribute.cs new file mode 100644 index 0000000000..1555a24f7f --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/Annotations/HasManyAttribute.cs @@ -0,0 +1,28 @@ +using System; + +namespace JsonApiDotNetCore.Resources.Annotations +{ + /// + /// Used to expose a property on a resource class as a json:api to-many relationship (https://jsonapi.org/format/#document-resource-object-relationships). + /// + [AttributeUsage(AttributeTargets.Property)] + public class HasManyAttribute : RelationshipAttribute + { + /// + /// Creates a HasMany relational link to another resource. + /// + /// + /// Articles { get; set; } + /// } + /// ]]> + /// + public HasManyAttribute() + { + Links = LinkTypes.All; + } + } +} diff --git a/src/JsonApiDotNetCore/Resources/Annotations/HasManyThroughAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/HasManyThroughAttribute.cs new file mode 100644 index 0000000000..409e4d639a --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/Annotations/HasManyThroughAttribute.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace JsonApiDotNetCore.Resources.Annotations +{ + /// + /// Used to expose a property on a resource class as a json:api to-many relationship (https://jsonapi.org/format/#document-resource-object-relationships) + /// through a many-to-many join relationship. + /// + /// + /// In the following example, we expose a relationship named "tags" + /// through the navigation property `ArticleTags`. + /// The `Tags` property is decorated with `NotMapped` so that EF does not try + /// to map this to a database relationship. + /// Tags { get; set; } + /// public ISet ArticleTags { get; set; } + /// } + /// + /// public class Tag : Identifiable + /// { + /// [Attr] + /// public string Name { get; set; } + /// } + /// + /// public sealed class ArticleTag + /// { + /// public int ArticleId { get; set; } + /// public Article Article { get; set; } + /// + /// public int TagId { get; set; } + /// public Tag Tag { get; set; } + /// } + /// ]]> + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class HasManyThroughAttribute : HasManyAttribute + { + /// + /// The name of the join property on the parent resource. + /// In the example described above, this would be "ArticleTags". + /// + internal string ThroughPropertyName { get; } + + /// + /// The join type. + /// In the example described above, this would be `ArticleTag`. + /// + public Type ThroughType { get; internal set; } + + /// + /// The navigation property back to the parent resource from the through type. + /// In the example described above, this would point to the `Article.ArticleTags.Article` property. + /// + public PropertyInfo LeftProperty { get; internal set; } + + /// + /// The ID property back to the parent resource from the through type. + /// In the example described above, this would point to the `Article.ArticleTags.ArticleId` property. + /// + public PropertyInfo LeftIdProperty { get; internal set; } + + /// + /// The navigation property to the related resource from the through type. + /// In the example described above, this would point to the `Article.ArticleTags.Tag` property. + /// + public PropertyInfo RightProperty { get; internal set; } + + /// + /// The ID property to the related resource from the through type. + /// In the example described above, this would point to the `Article.ArticleTags.TagId` property. + /// + public PropertyInfo RightIdProperty { get; internal set; } + + /// + /// The join resource property on the parent resource. + /// In the example described above, this would point to the `Article.ArticleTags` property. + /// + public PropertyInfo ThroughProperty { get; internal set; } + + /// + /// The internal navigation property path to the related resource. + /// In the example described above, this would contain "ArticleTags.Tag". + /// + public override string RelationshipPath => $"{ThroughProperty.Name}.{RightProperty.Name}"; + + /// + /// Creates a HasMany relationship through a many-to-many join relationship. + /// + /// The name of the navigation property that will be used to access the join relationship. + public HasManyThroughAttribute(string throughPropertyName) + { + ThroughPropertyName = throughPropertyName ?? throw new ArgumentNullException(nameof(throughPropertyName)); + } + + /// + /// Traverses through the provided resource and returns the value of the relationship on the other side of the through type. + /// In the example described above, this would be the value of "Articles.ArticleTags.Tag". + /// + public override object GetValue(object resource) + { + if (resource == null) throw new ArgumentNullException(nameof(resource)); + + IEnumerable throughResources = (IEnumerable)ThroughProperty.GetValue(resource) ?? Array.Empty(); + + IEnumerable rightResources = throughResources + .Cast() + .Select(rightResource => RightProperty.GetValue(rightResource)); + + return TypeHelper.CopyToTypedCollection(rightResources, Property.PropertyType); + } + + /// + /// Traverses through the provided resource and sets the value of the relationship on the other side of the through type. + /// In the example described above, this would be the value of "Articles.ArticleTags.Tag". + /// + public override void SetValue(object resource, object newValue, IResourceFactory resourceFactory) + { + if (resource == null) throw new ArgumentNullException(nameof(resource)); + if (resourceFactory == null) throw new ArgumentNullException(nameof(resourceFactory)); + + base.SetValue(resource, newValue, resourceFactory); + + if (newValue == null) + { + ThroughProperty.SetValue(resource, null); + } + else + { + List throughResources = new List(); + foreach (IIdentifiable identifiable in (IEnumerable)newValue) + { + object throughResource = resourceFactory.CreateInstance(ThroughType); + LeftProperty.SetValue(throughResource, resource); + RightProperty.SetValue(throughResource, identifiable); + throughResources.Add(throughResource); + } + + var typedCollection = TypeHelper.CopyToTypedCollection(throughResources, ThroughProperty.PropertyType); + ThroughProperty.SetValue(resource, typedCollection); + } + } + } +} diff --git a/src/JsonApiDotNetCore/Resources/Annotations/HasOneAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/HasOneAttribute.cs new file mode 100644 index 0000000000..d0e739aef9 --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/Annotations/HasOneAttribute.cs @@ -0,0 +1,62 @@ +using System; +using JsonApiDotNetCore.Configuration; + +namespace JsonApiDotNetCore.Resources.Annotations +{ + /// + /// Used to expose a property on a resource class as a json:api to-one relationship (https://jsonapi.org/format/#document-resource-object-relationships). + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class HasOneAttribute : RelationshipAttribute + { + private string _identifiablePropertyName; + + /// + /// The foreign key property name. Defaults to "{RelationshipName}Id". + /// + /// + /// Using an alternative foreign key: + /// + /// public class Article : Identifiable + /// { + /// [HasOne(PublicName = "author", IdentifiablePropertyName = nameof(AuthorKey)] + /// public Author Author { get; set; } + /// public int AuthorKey { get; set; } + /// } + /// + /// + public string IdentifiablePropertyName + { + get => _identifiablePropertyName ?? JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(Property.Name); + set => _identifiablePropertyName = value; + } + + public HasOneAttribute() + { + Links = LinkTypes.NotConfigured; + } + + /// + public override void SetValue(object resource, object newValue, IResourceFactory resourceFactory) + { + if (resource == null) throw new ArgumentNullException(nameof(resource)); + if (resourceFactory == null) throw new ArgumentNullException(nameof(resourceFactory)); + + // If we're deleting the relationship (setting it to null), we set the foreignKey to null. + // We could also set the actual property to null, but then we would first need to load the + // current relationship, which requires an extra query. + + var propertyName = newValue == null ? IdentifiablePropertyName : Property.Name; + var resourceType = resource.GetType(); + + var propertyInfo = resourceType.GetProperty(propertyName); + if (propertyInfo == null) + { + // we can't set the FK to null because there isn't any. + propertyInfo = resourceType.GetProperty(RelationshipPath); + } + + propertyInfo.SetValue(resource, newValue); + } + } +} diff --git a/src/JsonApiDotNetCore/Models/IsRequiredAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/IsRequiredAttribute.cs similarity index 66% rename from src/JsonApiDotNetCore/Models/IsRequiredAttribute.cs rename to src/JsonApiDotNetCore/Resources/Annotations/IsRequiredAttribute.cs index d28c106e09..71fbd8faa9 100644 --- a/src/JsonApiDotNetCore/Models/IsRequiredAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/IsRequiredAttribute.cs @@ -1,21 +1,29 @@ +using System; using System.ComponentModel.DataAnnotations; +using JsonApiDotNetCore.Middleware; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -using JsonApiDotNetCore.Extensions; -namespace JsonApiDotNetCore.Models +namespace JsonApiDotNetCore.Resources.Annotations { + /// + /// Used with model state validation as a replacement for the built-in to support partial updates. + /// public sealed class IsRequiredAttribute : RequiredAttribute { private bool _isDisabled; + /// public override bool IsValid(object value) { return _isDisabled || base.IsValid(value); } + /// protected override ValidationResult IsValid(object value, ValidationContext validationContext) { + if (validationContext == null) throw new ArgumentNullException(nameof(validationContext)); + var httpContextAccessor = (IHttpContextAccessor)validationContext.GetRequiredService(typeof(IHttpContextAccessor)); _isDisabled = httpContextAccessor.HttpContext.IsValidatorDisabled(validationContext.MemberName, validationContext.ObjectType.Name); return _isDisabled ? ValidationResult.Success : base.IsValid(value, validationContext); diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/Links.cs b/src/JsonApiDotNetCore/Resources/Annotations/LinkTypes.cs similarity index 73% rename from src/JsonApiDotNetCore/Models/JsonApiDocuments/Links.cs rename to src/JsonApiDotNetCore/Resources/Annotations/LinkTypes.cs index f5d4b18d05..d655a8b882 100644 --- a/src/JsonApiDotNetCore/Models/JsonApiDocuments/Links.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/LinkTypes.cs @@ -1,9 +1,9 @@ using System; -namespace JsonApiDotNetCore.Models.JsonApiDocuments +namespace JsonApiDotNetCore.Resources.Annotations { [Flags] - public enum Links + public enum LinkTypes { Self = 1 << 0, Related = 1 << 1, diff --git a/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs new file mode 100644 index 0000000000..b13f8dd852 --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs @@ -0,0 +1,108 @@ +using System; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; + +namespace JsonApiDotNetCore.Resources.Annotations +{ + /// + /// Used to expose a property on a resource class as a json:api relationship (https://jsonapi.org/format/#document-resource-object-relationships). + /// + public abstract class RelationshipAttribute : ResourceFieldAttribute + { + private LinkTypes _links; + + public string InverseNavigation { get; set; } + + /// + /// The internal navigation property path to the related resource. + /// + /// + /// In all cases except for relationships, this equals the property name. + /// + public virtual string RelationshipPath => Property.Name; + + /// + /// The child resource type. This does not necessarily match the navigation property type. + /// In the case of a relationship, this value will be the collection argument type. + /// + /// + /// Tags { get; set; } // Type => Tag + /// ]]> + /// + public Type RightType { get; internal set; } + + /// + /// The parent resource type. This is the type of the class in which this attribute was used. + /// + public Type LeftType { get; internal set; } + + /// + /// Configures which links to show in the object for this relationship. + /// When not explicitly assigned, the default value depends on the relationship type (see remarks). + /// + /// + /// This defaults to for and relationships. + /// This defaults to for relationships, which means that + /// the configuration in or is used. + /// + public LinkTypes Links + { + get => _links; + set + { + if (value == LinkTypes.Paging) + { + throw new InvalidConfigurationException($"{LinkTypes.Paging:g} not allowed for argument {nameof(value)}"); + } + + _links = value; + } + } + + /// + /// Whether or not this relationship can be included using the ?include=publicName query string parameter. + /// This is true by default. + /// + public bool CanInclude { get; set; } = true; + + /// + /// Gets the value of the resource property this attributes was declared on. + /// + public virtual object GetValue(object resource) + { + if (resource == null) throw new ArgumentNullException(nameof(resource)); + + return Property.GetValue(resource); + } + + /// + /// Sets the value of the resource property this attributes was declared on. + /// + public virtual void SetValue(object resource, object newValue, IResourceFactory resourceFactory) + { + if (resource == null) throw new ArgumentNullException(nameof(resource)); + if (resourceFactory == null) throw new ArgumentNullException(nameof(resourceFactory)); + + Property.SetValue(resource, newValue); + } + + public override bool Equals(object obj) + { + if (obj == null || GetType() != obj.GetType()) + { + return false; + } + + var other = (RelationshipAttribute) obj; + + return PublicName == other.PublicName && LeftType == other.LeftType && + RightType == other.RightType; + } + + public override int GetHashCode() + { + return HashCode.Combine(PublicName, LeftType, RightType); + } + } +} diff --git a/src/JsonApiDotNetCore/Resources/Annotations/ResourceAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/ResourceAttribute.cs new file mode 100644 index 0000000000..7b8a3a6ad0 --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/Annotations/ResourceAttribute.cs @@ -0,0 +1,22 @@ +using System; + +namespace JsonApiDotNetCore.Resources.Annotations +{ + /// + /// When put on a resource class, overrides the convention-based resource name. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] + public sealed class ResourceAttribute : Attribute + { + /// + /// The publicly exposed name of this resource type. + /// When not explicitly assigned, the configured casing convention is applied on the pluralized resource class name. + /// + public string PublicName { get; } + + public ResourceAttribute(string publicName) + { + PublicName = publicName ?? throw new ArgumentNullException(nameof(publicName)); + } + } +} diff --git a/src/JsonApiDotNetCore/Resources/Annotations/ResourceFieldAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/ResourceFieldAttribute.cs new file mode 100644 index 0000000000..d9688ae52e --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/Annotations/ResourceFieldAttribute.cs @@ -0,0 +1,41 @@ +using System; +using System.Reflection; + +namespace JsonApiDotNetCore.Resources.Annotations +{ + /// + /// Used to expose a property on a resource class as a json:api field (attribute or relationship). + /// See https://jsonapi.org/format/#document-resource-object-fields. + /// + public abstract class ResourceFieldAttribute : Attribute + { + private string _publicName; + + /// + /// The publicly exposed name of this json:api field. + /// When not explicitly assigned, the configured casing convention is applied on the property name. + /// + public string PublicName + { + get => _publicName; + set + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Exposed name cannot be null, empty or contain only whitespace.", nameof(value)); + } + _publicName = value; + } + } + + /// + /// The resource property that this attribute is declared on. + /// + public PropertyInfo Property { get; internal set; } + + public override string ToString() + { + return PublicName ?? (Property != null ? Property.Name : base.ToString()); + } + } +} diff --git a/src/JsonApiDotNetCore/Resources/Annotations/ResourceLinksAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/ResourceLinksAttribute.cs new file mode 100644 index 0000000000..eff732d954 --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/Annotations/ResourceLinksAttribute.cs @@ -0,0 +1,75 @@ +using System; +using JsonApiDotNetCore.Errors; + +namespace JsonApiDotNetCore.Resources.Annotations +{ + // TODO: There are no tests for this. + + /// + /// When put on a resource class, overrides global configuration for which links to render. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] + public sealed class ResourceLinksAttribute : Attribute + { + private LinkTypes _topLevelLinks = LinkTypes.NotConfigured; + private LinkTypes _resourceLinks = LinkTypes.NotConfigured; + private LinkTypes _relationshipLinks = LinkTypes.NotConfigured; + + /// + /// Configures which links to show in the + /// section for this resource. + /// Defaults to . + /// + public LinkTypes TopLevelLinks + { + get => _topLevelLinks; + set + { + if (value == LinkTypes.Related) + { + throw new InvalidConfigurationException($"{LinkTypes.Related:g} not allowed for argument {nameof(value)}"); + } + + _topLevelLinks = value; + } + } + + /// + /// Configures which links to show in the + /// section for this resource. + /// Defaults to . + /// + public LinkTypes ResourceLinks + { + get => _resourceLinks; + set + { + if (value == LinkTypes.Paging) + { + throw new InvalidConfigurationException($"{LinkTypes.Paging:g} not allowed for argument {nameof(value)}"); + } + + _resourceLinks = value; + } + } + + /// + /// Configures which links to show in the + /// for all relationships of the resource type on which this attribute was used. + /// Defaults to . + /// + public LinkTypes RelationshipLinks + { + get => _relationshipLinks; + set + { + if (value == LinkTypes.Paging) + { + throw new InvalidConfigurationException($"{LinkTypes.Paging:g} not allowed for argument {nameof(value)}"); + } + + _relationshipLinks = value; + } + } + } +} diff --git a/src/JsonApiDotNetCore/Resources/IHasMeta.cs b/src/JsonApiDotNetCore/Resources/IHasMeta.cs new file mode 100644 index 0000000000..a22408d78d --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/IHasMeta.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace JsonApiDotNetCore.Resources +{ + /// + /// When implemented by a class, indicates it provides json:api meta key/value pairs. + /// + public interface IHasMeta + { + IReadOnlyDictionary GetMeta(); + } +} diff --git a/src/JsonApiDotNetCore/Resources/IIdentifiable.cs b/src/JsonApiDotNetCore/Resources/IIdentifiable.cs new file mode 100644 index 0000000000..f8e8d9f613 --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/IIdentifiable.cs @@ -0,0 +1,26 @@ +namespace JsonApiDotNetCore.Resources +{ + /// + /// When implemented by a class, indicates to JsonApiDotNetCore that the class represents a json:api resource. + /// Note that JsonApiDotNetCore also assumes that a property named 'Id' exists. + /// + public interface IIdentifiable + { + /// + /// The value for element 'id' in a json:api request or response. + /// + string StringId { get; set; } + } + + /// + /// When implemented by a class, indicates to JsonApiDotNetCore that the class represents a json:api resource. + /// + /// The resource identifier type. + public interface IIdentifiable : IIdentifiable + { + /// + /// The typed identifier as used by the underlying data store (usually numeric). + /// + TId Id { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Resources/IResourceChangeTracker.cs b/src/JsonApiDotNetCore/Resources/IResourceChangeTracker.cs new file mode 100644 index 0000000000..82aae71710 --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/IResourceChangeTracker.cs @@ -0,0 +1,32 @@ +namespace JsonApiDotNetCore.Resources +{ + /// + /// Used to determine whether additional changes to a resource, not specified in a PATCH request, have been applied. + /// + public interface IResourceChangeTracker where TResource : class, IIdentifiable + { + /// + /// Sets the exposed resource attributes as stored in database, before applying changes. + /// + void SetInitiallyStoredAttributeValues(TResource resource); + + /// + /// Sets the subset of exposed attributes from the PATCH request. + /// + void SetRequestedAttributeValues(TResource resource); + + /// + /// Sets the exposed resource attributes as stored in database, after applying changes. + /// + void SetFinallyStoredAttributeValues(TResource resource); + + /// + /// Validates if any exposed resource attributes that were not in the PATCH request have been changed. + /// And validates if the values from the PATCH request are stored without modification. + /// + /// + /// true if the attribute values from the PATCH request were the only changes; false, otherwise. + /// + bool HasImplicitChanges(); + } +} diff --git a/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs new file mode 100644 index 0000000000..a192e103b0 --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Queries.Expressions; + +namespace JsonApiDotNetCore.Resources +{ + /// + /// Used internally to track resource extensibility endpoints. Do not implement this interface directly. + /// + public interface IResourceDefinition + { + IReadOnlyCollection OnApplyIncludes(IReadOnlyCollection existingIncludes); + FilterExpression OnApplyFilter(FilterExpression existingFilter); + SortExpression OnApplySort(SortExpression existingSort); + PaginationExpression OnApplyPagination(PaginationExpression existingPagination); + SparseFieldSetExpression OnApplySparseFieldSet(SparseFieldSetExpression existingSparseFieldSet); + object GetQueryableHandlerForQueryStringParameter(string parameterName); + } +} diff --git a/src/JsonApiDotNetCore/Services/Contract/IResourceDefinitionProvider.cs b/src/JsonApiDotNetCore/Resources/IResourceDefinitionProvider.cs similarity index 80% rename from src/JsonApiDotNetCore/Services/Contract/IResourceDefinitionProvider.cs rename to src/JsonApiDotNetCore/Resources/IResourceDefinitionProvider.cs index 85281714ab..07a17148a4 100644 --- a/src/JsonApiDotNetCore/Services/Contract/IResourceDefinitionProvider.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinitionProvider.cs @@ -1,7 +1,6 @@ using System; -using JsonApiDotNetCore.Models; -namespace JsonApiDotNetCore.Services.Contract +namespace JsonApiDotNetCore.Resources { /// /// Retrieves a from the DI container. @@ -13,7 +12,6 @@ public interface IResourceDefinitionProvider /// /// Retrieves the resource definition associated to . /// - /// IResourceDefinition Get(Type resourceType); } } diff --git a/src/JsonApiDotNetCore/Resources/IResourceFactory.cs b/src/JsonApiDotNetCore/Resources/IResourceFactory.cs new file mode 100644 index 0000000000..1ed2356ff7 --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/IResourceFactory.cs @@ -0,0 +1,26 @@ +using System; +using System.Linq.Expressions; + +namespace JsonApiDotNetCore.Resources +{ + /// + /// Creates object instances for resource classes, which may have injectable dependencies. + /// + public interface IResourceFactory + { + /// + /// Creates a new resource object instance. + /// + public object CreateInstance(Type resourceType); + + /// + /// Creates a new resource object instance. + /// + public TResource CreateInstance(); + + /// + /// Returns an expression tree that represents creating a new resource object instance. + /// + public NewExpression CreateNewExpression(Type resourceType); + } +} diff --git a/src/JsonApiDotNetCore/Resources/ITargetedFields.cs b/src/JsonApiDotNetCore/Resources/ITargetedFields.cs new file mode 100644 index 0000000000..03262e834d --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/ITargetedFields.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCore.Resources +{ + /// + /// Container to register which resource attributes and relationships are targeted by a request. + /// + public interface ITargetedFields + { + /// + /// List of attributes that are targeted by a request. + /// + IList Attributes { get; set; } + + /// + /// List of relationships that are targeted by a request. + /// + IList Relationships { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Resources/Identifiable.cs b/src/JsonApiDotNetCore/Resources/Identifiable.cs new file mode 100644 index 0000000000..b621869e80 --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/Identifiable.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace JsonApiDotNetCore.Resources +{ + /// + public abstract class Identifiable : Identifiable + { } + + /// + /// A convenient basic implementation of that provides conversion between and . + /// + /// The resource identifier type. + public abstract class Identifiable : IIdentifiable + { + /// + public virtual TId Id { get; set; } + + /// + [NotMapped] + public string StringId + { + get => GetStringId(Id); + set => Id = GetTypedId(value); + } + + /// + /// Converts an outgoing typed resource identifier to string format for use in a json:api response. + /// + protected virtual string GetStringId(TId value) + { + return EqualityComparer.Default.Equals(value, default) ? null : value.ToString(); + } + + /// + /// Converts an incoming 'id' element from a json:api request to the typed resource identifier. + /// + protected virtual TId GetTypedId(string value) + { + return string.IsNullOrEmpty(value) ? default : (TId)TypeHelper.ConvertType(value, typeof(TId)); + } + } +} diff --git a/src/JsonApiDotNetCore/RequestServices/ResourceChangeTracker.cs b/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs similarity index 65% rename from src/JsonApiDotNetCore/RequestServices/ResourceChangeTracker.cs rename to src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs index 5e5199d95e..9df7cd9dd4 100644 --- a/src/JsonApiDotNetCore/RequestServices/ResourceChangeTracker.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs @@ -1,43 +1,12 @@ +using System; using System.Collections.Generic; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Resources.Annotations; using Newtonsoft.Json; -namespace JsonApiDotNetCore.RequestServices +namespace JsonApiDotNetCore.Resources { - /// - /// Used to determine whether additional changes to a resource, not specified in a PATCH request, have been applied. - /// - public interface IResourceChangeTracker where TResource : class, IIdentifiable - { - /// - /// Sets the exposed resource attributes as stored in database, before applying changes. - /// - void SetInitiallyStoredAttributeValues(TResource resource); - - /// - /// Sets the subset of exposed attributes from the PATCH request. - /// - void SetRequestedAttributeValues(TResource resource); - - /// - /// Sets the exposed resource attributes as stored in database, after applying changes. - /// - void SetFinallyStoredAttributeValues(TResource resource); - - /// - /// Validates if any exposed resource attributes that were not in the PATCH request have been changed. - /// And validates if the values from the PATCH request are stored without modification. - /// - /// - /// true if the attribute values from the PATCH request were the only changes; false, otherwise. - /// - bool HasImplicitChanges(); - } - + /// public sealed class ResourceChangeTracker : IResourceChangeTracker where TResource : class, IIdentifiable { private readonly IJsonApiOptions _options; @@ -51,24 +20,33 @@ public sealed class ResourceChangeTracker : IResourceChangeTracker public void SetInitiallyStoredAttributeValues(TResource resource) { + if (resource == null) throw new ArgumentNullException(nameof(resource)); + var resourceContext = _contextProvider.GetResourceContext(); _initiallyStoredAttributeValues = CreateAttributeDictionary(resource, resourceContext.Attributes); } + /// public void SetRequestedAttributeValues(TResource resource) { + if (resource == null) throw new ArgumentNullException(nameof(resource)); + _requestedAttributeValues = CreateAttributeDictionary(resource, _targetedFields.Attributes); } + /// public void SetFinallyStoredAttributeValues(TResource resource) { + if (resource == null) throw new ArgumentNullException(nameof(resource)); + var resourceContext = _contextProvider.GetResourceContext(); _finallyStoredAttributeValues = CreateAttributeDictionary(resource, resourceContext.Attributes); } @@ -88,6 +66,7 @@ private IDictionary CreateAttributeDictionary(TResource resource return result; } + /// public bool HasImplicitChanges() { foreach (var key in _initiallyStoredAttributeValues.Keys) diff --git a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/ResourceDefinition.cs similarity index 78% rename from src/JsonApiDotNetCore/Models/ResourceDefinition.cs rename to src/JsonApiDotNetCore/Resources/ResourceDefinition.cs index 086905e16f..ee446b9334 100644 --- a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceDefinition.cs @@ -1,32 +1,22 @@ -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Hooks; using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Linq.Expressions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Hooks.Internal; +using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCore.Queries.Expressions; using Microsoft.Extensions.Primitives; -namespace JsonApiDotNetCore.Models +namespace JsonApiDotNetCore.Resources { - public interface IResourceDefinition - { - IReadOnlyCollection OnApplyIncludes(IReadOnlyCollection existingIncludes); - FilterExpression OnApplyFilter(FilterExpression existingFilter); - SortExpression OnApplySort(SortExpression existingSort); - PaginationExpression OnApplyPagination(PaginationExpression existingPagination); - SparseFieldSetExpression OnApplySparseFieldSet(SparseFieldSetExpression existingSparseFieldSet); - object GetQueryableHandlerForQueryStringParameter(string parameterName); - } - /// - /// Exposes developer friendly hooks into how their resources are exposed. - /// It is intended to improve the experience and reduce boilerplate for commonly required features. - /// The goal of this class is to reduce the frequency with which developers have to override the - /// service and repository layers. + /// Provides a resource-specific extensibility point for API developers to be notified of various events and influence behavior using custom code. + /// It is intended to improve the developer experience and reduce boilerplate for commonly required features. + /// The goal of this class is to reduce the frequency with which developers have to override the service and repository layers. /// - /// The resource type + /// The resource type. public class ResourceDefinition : IResourceDefinition, IResourceHookContainer where TResource : class, IIdentifiable { protected IResourceGraph ResourceGraph { get; } @@ -36,29 +26,52 @@ public ResourceDefinition(IResourceGraph resourceGraph) ResourceGraph = resourceGraph ?? throw new ArgumentNullException(nameof(resourceGraph)); } - /// + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. public virtual void AfterCreate(HashSet resources, ResourcePipeline pipeline) { } - /// + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. public virtual void AfterRead(HashSet resources, ResourcePipeline pipeline, bool isIncluded = false) { } - /// + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. public virtual void AfterUpdate(HashSet resources, ResourcePipeline pipeline) { } - /// + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. public virtual void AfterDelete(HashSet resources, ResourcePipeline pipeline, bool succeeded) { } - /// + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. public virtual void AfterUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) { } - /// + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. public virtual IEnumerable BeforeCreate(IResourceHashSet resources, ResourcePipeline pipeline) { return resources; } - /// + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. public virtual void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) { } - /// + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. public virtual IEnumerable BeforeUpdate(IDiffableResourceHashSet resources, ResourcePipeline pipeline) { return resources; } - /// + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. public virtual IEnumerable BeforeDelete(IResourceHashSet resources, ResourcePipeline pipeline) { return resources; } - /// + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. public virtual IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) { return ids; } - /// + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. public virtual void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) { } - /// + + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. public virtual IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline) { return resources; } /// @@ -198,7 +211,6 @@ public virtual SparseFieldSetExpression OnApplySparseFieldSet(SparseFieldSetExpr /// } /// ]]> /// - /// protected virtual QueryStringParameterHandlers OnRegisterQueryableHandlersForQueryStringParameters() { return new QueryStringParameterHandlers(); @@ -206,6 +218,8 @@ protected virtual QueryStringParameterHandlers OnRegisterQueryableHandlersForQue public object GetQueryableHandlerForQueryStringParameter(string parameterName) { + if (parameterName == null) throw new ArgumentNullException(nameof(parameterName)); + var handlers = OnRegisterQueryableHandlersForQueryStringParameters(); return handlers != null && handlers.ContainsKey(parameterName) ? handlers[parameterName] : null; } diff --git a/src/JsonApiDotNetCore/Resources/ResourceDefinitionProvider.cs b/src/JsonApiDotNetCore/Resources/ResourceDefinitionProvider.cs new file mode 100644 index 0000000000..74e41aa304 --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/ResourceDefinitionProvider.cs @@ -0,0 +1,27 @@ +using System; +using JsonApiDotNetCore.Configuration; + +namespace JsonApiDotNetCore.Resources +{ + /// + internal sealed class ResourceDefinitionProvider : IResourceDefinitionProvider + { + private readonly IResourceGraph _resourceContextProvider; + private readonly IRequestScopedServiceProvider _serviceProvider; + + public ResourceDefinitionProvider(IResourceGraph resourceContextProvider, IRequestScopedServiceProvider serviceProvider) + { + _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + } + + /// + public IResourceDefinition Get(Type resourceType) + { + if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + + var resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + return (IResourceDefinition)_serviceProvider.GetService(resourceContext.ResourceDefinitionType); + } + } +} diff --git a/src/JsonApiDotNetCore/Internal/IResourceFactory.cs b/src/JsonApiDotNetCore/Resources/ResourceFactory.cs similarity index 64% rename from src/JsonApiDotNetCore/Internal/IResourceFactory.cs rename to src/JsonApiDotNetCore/Resources/ResourceFactory.cs index 230aa25e8a..7ec213e134 100644 --- a/src/JsonApiDotNetCore/Internal/IResourceFactory.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceFactory.cs @@ -1,19 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using System.Reflection; -using JsonApiDotNetCore.Extensions; using Microsoft.Extensions.DependencyInjection; -namespace JsonApiDotNetCore.Internal +namespace JsonApiDotNetCore.Resources { - public interface IResourceFactory - { - public object CreateInstance(Type resourceType); - public TResource CreateInstance(); - public NewExpression CreateNewExpression(Type resourceType); - } - + /// internal sealed class ResourceFactory : IResourceFactory { private readonly IServiceProvider _serviceProvider; @@ -23,6 +17,7 @@ public ResourceFactory(IServiceProvider serviceProvider) _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); } + /// public object CreateInstance(Type resourceType) { if (resourceType == null) @@ -33,6 +28,7 @@ public object CreateInstance(Type resourceType) return InnerCreateInstance(resourceType, _serviceProvider); } + /// public TResource CreateInstance() { return (TResource) InnerCreateInstance(typeof(TResource), _serviceProvider); @@ -40,7 +36,7 @@ public TResource CreateInstance() private static object InnerCreateInstance(Type type, IServiceProvider serviceProvider) { - bool hasSingleConstructorWithoutParameters = type.HasSingleConstructorWithoutParameters(); + bool hasSingleConstructorWithoutParameters = HasSingleConstructorWithoutParameters(type); try { @@ -57,16 +53,19 @@ private static object InnerCreateInstance(Type type, IServiceProvider servicePro } } + /// public NewExpression CreateNewExpression(Type resourceType) { - if (resourceType.HasSingleConstructorWithoutParameters()) + if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + + if (HasSingleConstructorWithoutParameters(resourceType)) { return Expression.New(resourceType); } List constructorArguments = new List(); - var longestConstructor = resourceType.GetLongestConstructor(); + var longestConstructor = GetLongestConstructor(resourceType); foreach (ParameterInfo constructorParameter in longestConstructor.GetParameters()) { try @@ -86,5 +85,33 @@ public NewExpression CreateNewExpression(Type resourceType) return Expression.New(longestConstructor, constructorArguments); } + + private static bool HasSingleConstructorWithoutParameters(Type type) + { + ConstructorInfo[] constructors = type.GetConstructors().Where(c => !c.IsStatic).ToArray(); + + return constructors.Length == 1 && constructors[0].GetParameters().Length == 0; + } + + private static ConstructorInfo GetLongestConstructor(Type type) + { + ConstructorInfo[] constructors = type.GetConstructors().Where(c => !c.IsStatic).ToArray(); + + ConstructorInfo bestMatch = constructors[0]; + int maxParameterLength = constructors[0].GetParameters().Length; + + for (int index = 1; index < constructors.Length; index++) + { + var constructor = constructors[index]; + int length = constructor.GetParameters().Length; + if (length > maxParameterLength) + { + bestMatch = constructor; + maxParameterLength = length; + } + } + + return bestMatch; + } } } diff --git a/src/JsonApiDotNetCore/Resources/TargetedFields.cs b/src/JsonApiDotNetCore/Resources/TargetedFields.cs new file mode 100644 index 0000000000..6784b8b9c8 --- /dev/null +++ b/src/JsonApiDotNetCore/Resources/TargetedFields.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCore.Resources +{ + /// + public sealed class TargetedFields : ITargetedFields + { + /// + public IList Attributes { get; set; } = new List(); + + /// + public IList Relationships { get; set; } = new List(); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs b/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs similarity index 68% rename from src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs rename to src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs index 6e705a8c41..cbe4b793c8 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs +++ b/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs @@ -3,75 +3,76 @@ using System.IO; using System.Linq; using System.Reflection; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Serialization.Client; -using JsonApiDotNetCore.Serialization.Server; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Client.Internal; +using JsonApiDotNetCore.Serialization.Objects; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace JsonApiDotNetCore.Serialization { /// - /// Abstract base class for deserialization. Deserializes JSON content into s - /// And constructs instances of the resource(s) in the document body. + /// Abstract base class for deserialization. Deserializes JSON content into s + /// and constructs instances of the resource(s) in the document body. /// - public abstract class BaseDocumentParser + public abstract class BaseDeserializer { - protected readonly IResourceContextProvider _contextProvider; - protected readonly IResourceFactory _resourceFactory; - protected Document _document; + protected IResourceContextProvider ResourceContextProvider { get; } + protected IResourceFactory ResourceFactory{ get; } + protected Document Document { get; set; } - protected BaseDocumentParser(IResourceContextProvider contextProvider, IResourceFactory resourceFactory) + protected BaseDeserializer(IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory) { - _contextProvider = contextProvider; - _resourceFactory = resourceFactory; + ResourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); + ResourceFactory = resourceFactory ?? throw new ArgumentNullException(nameof(resourceFactory)); } /// - /// This method is called each time an is constructed + /// This method is called each time a is constructed /// from the serialized content, which is used to do additional processing - /// depending on the type of deserializers. + /// depending on the type of deserializer. /// /// /// See the implementation of this method in /// and for examples. /// - /// The resource that was constructed from the document's body - /// The metadata for the exposed field - /// Relationship data for . Is null when is not a + /// The resource that was constructed from the document's body. + /// The metadata for the exposed field. + /// Relationship data for . Is null when is not a . protected abstract void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipEntry data = null); - /// - protected object Deserialize(string body) + protected object DeserializeBody(string body) { + if (body == null) throw new ArgumentNullException(nameof(body)); + var bodyJToken = LoadJToken(body); - _document = bodyJToken.ToObject(); - if (_document.IsManyData) + Document = bodyJToken.ToObject(); + if (Document.IsManyData) { - if (_document.ManyData.Count == 0) + if (Document.ManyData.Count == 0) return Array.Empty(); - return _document.ManyData.Select(ParseResourceObject).ToList(); + return Document.ManyData.Select(ParseResourceObject).ToArray(); } - if (_document.SingleData == null) return null; - return ParseResourceObject(_document.SingleData); + if (Document.SingleData == null) return null; + return ParseResourceObject(Document.SingleData); } /// /// Sets the attributes on a parsed resource. /// - /// The parsed resource - /// Attributes and their values, as in the serialized content - /// Exposed attributes for - /// - protected virtual IIdentifiable SetAttributes(IIdentifiable resource, Dictionary attributeValues, List attributes) + /// The parsed resource. + /// Attributes and their values, as in the serialized content. + /// Exposed attributes for . + protected virtual IIdentifiable SetAttributes(IIdentifiable resource, IDictionary attributeValues, IReadOnlyCollection attributes) { + if (resource == null) throw new ArgumentNullException(nameof(resource)); + if (attributes == null) throw new ArgumentNullException(nameof(attributes)); + if (attributeValues == null || attributeValues.Count == 0) return resource; @@ -89,21 +90,23 @@ protected virtual IIdentifiable SetAttributes(IIdentifiable resource, Dictionary } /// - /// Sets the relationships on a parsed resource + /// Sets the relationships on a parsed resource. /// - /// The parsed resource - /// Relationships and their values, as in the serialized content - /// Exposed relationships for - /// - protected virtual IIdentifiable SetRelationships(IIdentifiable resource, Dictionary relationshipsValues, List relationshipAttributes) + /// The parsed resource. + /// Relationships and their values, as in the serialized content. + /// Exposed relationships for . + protected virtual IIdentifiable SetRelationships(IIdentifiable resource, IDictionary relationshipValues, IReadOnlyCollection relationshipAttributes) { - if (relationshipsValues == null || relationshipsValues.Count == 0) + if (resource == null) throw new ArgumentNullException(nameof(resource)); + if (relationshipAttributes == null) throw new ArgumentNullException(nameof(relationshipAttributes)); + + if (relationshipValues == null || relationshipValues.Count == 0) return resource; var resourceProperties = resource.GetType().GetProperties(); foreach (var attr in relationshipAttributes) { - if (!relationshipsValues.TryGetValue(attr.PublicName, out RelationshipEntry relationshipData) || !relationshipData.IsPopulated) + if (!relationshipValues.TryGetValue(attr.PublicName, out RelationshipEntry relationshipData) || !relationshipData.IsPopulated) continue; if (attr is HasOneAttribute hasOneAttribute) @@ -128,22 +131,21 @@ private JToken LoadJToken(string body) /// /// Creates an instance of the referenced type in - /// and sets its attributes and relationships + /// and sets its attributes and relationships. /// - /// - /// The parsed resource + /// The parsed resource. private IIdentifiable ParseResourceObject(ResourceObject data) { - var resourceContext = _contextProvider.GetResourceContext(data.Type); + var resourceContext = ResourceContextProvider.GetResourceContext(data.Type); if (resourceContext == null) { throw new InvalidRequestBodyException("Payload includes unknown resource type.", $"The resource '{data.Type}' is not registered on the resource graph. " + "If you are using Entity Framework Core, make sure the DbSet matches the expected resource name. " + - "If you have manually registered the resource, check that the call to AddResource correctly sets the public name.", null); + "If you have manually registered the resource, check that the call to Add correctly sets the public name.", null); } - var resource = (IIdentifiable)_resourceFactory.CreateInstance(resourceContext.ResourceType); + var resource = (IIdentifiable)ResourceFactory.CreateInstance(resourceContext.ResourceType); resource = SetAttributes(resource, data.Attributes, resourceContext.Attributes); resource = SetRelationships(resource, data.Relationships, resourceContext.Relationships); @@ -158,10 +160,6 @@ private IIdentifiable ParseResourceObject(ResourceObject data) /// Sets a HasOne relationship on a parsed resource. If present, also /// populates the foreign key. /// - /// - /// - /// - /// private void SetHasOneRelationship(IIdentifiable resource, PropertyInfo[] resourceProperties, HasOneAttribute attr, @@ -200,25 +198,25 @@ private void SetForeignKey(IIdentifiable resource, PropertyInfo foreignKey, HasO throw new FormatException($"Cannot set required relationship identifier '{attr.IdentifiablePropertyName}' to null because it is a non-nullable type."); } - var typedId = TypeHelper.ConvertStringIdToTypedId(attr.Property.PropertyType, id, _resourceFactory); + var typedId = TypeHelper.ConvertStringIdToTypedId(attr.Property.PropertyType, id, ResourceFactory); foreignKey.SetValue(resource, typedId); } /// /// Sets the principal side of a HasOne relationship, which means no - /// foreign key is involved + /// foreign key is involved. /// private void SetNavigation(IIdentifiable resource, HasOneAttribute attr, string relatedId) { if (relatedId == null) { - attr.SetValue(resource, null, _resourceFactory); + attr.SetValue(resource, null, ResourceFactory); } else { - var relatedInstance = (IIdentifiable)_resourceFactory.CreateInstance(attr.RightType); + var relatedInstance = (IIdentifiable)ResourceFactory.CreateInstance(attr.RightType); relatedInstance.StringId = relatedId; - attr.SetValue(resource, relatedInstance, _resourceFactory); + attr.SetValue(resource, relatedInstance, ResourceFactory); } } @@ -234,13 +232,13 @@ private void SetHasManyRelationship( { // if the relationship is set to null, no need to set the navigation property to null: this is the default value. var relatedResources = relationshipData.ManyData.Select(rio => { - var relatedInstance = (IIdentifiable)_resourceFactory.CreateInstance(attr.RightType); + var relatedInstance = (IIdentifiable)ResourceFactory.CreateInstance(attr.RightType); relatedInstance.StringId = rio.Id; return relatedInstance; }); - var convertedCollection = relatedResources.CopyToTypedCollection(attr.Property.PropertyType); - attr.SetValue(resource, convertedCollection, _resourceFactory); + var convertedCollection = TypeHelper.CopyToTypedCollection(relatedResources, attr.Property.PropertyType); + attr.SetValue(resource, convertedCollection, ResourceFactory); } AfterProcessField(resource, attr, relationshipData); diff --git a/src/JsonApiDotNetCore/Serialization/Common/DocumentBuilder.cs b/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs similarity index 55% rename from src/JsonApiDotNetCore/Serialization/Common/DocumentBuilder.cs rename to src/JsonApiDotNetCore/Serialization/BaseSerializer.cs index 148753cd95..da61a4b188 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs @@ -1,60 +1,66 @@ using System; using System.Collections.Generic; using System.IO; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Building; +using JsonApiDotNetCore.Serialization.Objects; using Newtonsoft.Json; namespace JsonApiDotNetCore.Serialization { /// /// Abstract base class for serialization. - /// Uses to convert resources in to s and wraps them in a . + /// Uses to convert resources into s and wraps them in a . /// - public abstract class BaseDocumentBuilder + public abstract class BaseSerializer { - protected readonly IResourceObjectBuilder _resourceObjectBuilder; + protected IResourceObjectBuilder ResourceObjectBuilder { get; } - protected BaseDocumentBuilder(IResourceObjectBuilder resourceObjectBuilder) + protected BaseSerializer(IResourceObjectBuilder resourceObjectBuilder) { - _resourceObjectBuilder = resourceObjectBuilder; + ResourceObjectBuilder = resourceObjectBuilder ?? throw new ArgumentNullException(nameof(resourceObjectBuilder)); } /// /// Builds a for . - /// Adds the attributes and relationships that are enlisted in and + /// Adds the attributes and relationships that are enlisted in and . /// - /// Resource to build a Resource Object for - /// Attributes to include in the building process - /// Relationships to include in the building process - /// The resource object that was built + /// Resource to build a for. + /// Attributes to include in the building process. + /// Relationships to include in the building process. + /// The resource object that was built. protected Document Build(IIdentifiable resource, IReadOnlyCollection attributes, IReadOnlyCollection relationships) { if (resource == null) return new Document(); - return new Document { Data = _resourceObjectBuilder.Build(resource, attributes, relationships) }; + return new Document { Data = ResourceObjectBuilder.Build(resource, attributes, relationships) }; } /// /// Builds a for . - /// Adds the attributes and relationships that are enlisted in and + /// Adds the attributes and relationships that are enlisted in and . /// - /// Resource to build a Resource Object for - /// Attributes to include in the building process - /// Relationships to include in the building process - /// The resource object that was built - protected Document Build(IEnumerable resources, IReadOnlyCollection attributes, IReadOnlyCollection relationships) + /// Resource to build a for. + /// Attributes to include in the building process. + /// Relationships to include in the building process. + /// The resource object that was built. + protected Document Build(IReadOnlyCollection resources, IReadOnlyCollection attributes, IReadOnlyCollection relationships) { + if (resources == null) throw new ArgumentNullException(nameof(resources)); + var data = new List(); foreach (IIdentifiable resource in resources) - data.Add(_resourceObjectBuilder.Build(resource, attributes, relationships)); + data.Add(ResourceObjectBuilder.Build(resource, attributes, relationships)); return new Document { Data = data }; } protected string SerializeObject(object value, JsonSerializerSettings defaultSettings, Action changeSerializer = null) { + if (defaultSettings == null) throw new ArgumentNullException(nameof(defaultSettings)); + JsonSerializer serializer = JsonSerializer.CreateDefault(defaultSettings); changeSerializer?.Invoke(serializer); diff --git a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IIncludedResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IIncludedResourceObjectBuilder.cs similarity index 55% rename from src/JsonApiDotNetCore/Serialization/Server/Contracts/IIncludedResourceObjectBuilder.cs rename to src/JsonApiDotNetCore/Serialization/Building/IIncludedResourceObjectBuilder.cs index 3a8f10acdc..88a1b21afc 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IIncludedResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/IIncludedResourceObjectBuilder.cs @@ -1,19 +1,21 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.Server.Builders +namespace JsonApiDotNetCore.Serialization.Building { public interface IIncludedResourceObjectBuilder { /// - /// Gets the list of resource objects representing the included resources + /// Gets the list of resource objects representing the included resources. /// - List Build(); + IList Build(); + /// /// Extracts the included resources from using the /// (arbitrarily deeply nested) included relationships in . /// - void IncludeRelationshipChain(List inclusionChain, IIdentifiable rootResource); + void IncludeRelationshipChain(IReadOnlyCollection inclusionChain, IIdentifiable rootResource); } } diff --git a/src/JsonApiDotNetCore/Serialization/Server/Contracts/ILinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ILinkBuilder.cs similarity index 74% rename from src/JsonApiDotNetCore/Serialization/Server/Contracts/ILinkBuilder.cs rename to src/JsonApiDotNetCore/Serialization/Building/ILinkBuilder.cs index b44bff6a2f..ee54290c92 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Contracts/ILinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ILinkBuilder.cs @@ -1,8 +1,8 @@ -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.Server.Builders +namespace JsonApiDotNetCore.Serialization.Building { /// /// Builds resource object links and relationship object links. @@ -20,8 +20,6 @@ public interface ILinkBuilder /// /// Builds the links object that is included in the values of the . /// - /// - /// RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable parent); } } diff --git a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IMetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IMetaBuilder.cs similarity index 51% rename from src/JsonApiDotNetCore/Serialization/Server/Contracts/IMetaBuilder.cs rename to src/JsonApiDotNetCore/Serialization/Building/IMetaBuilder.cs index 5e18f930a5..c939d9d139 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IMetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/IMetaBuilder.cs @@ -1,27 +1,27 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Resources; -namespace JsonApiDotNetCore.Serialization.Server.Builders +namespace JsonApiDotNetCore.Serialization.Building { /// - /// Builds the top-level meta data object. This builder is generic to allow for - /// different top-level meta data object depending on the associated resource of the request. + /// Builds the top-level meta object. This builder is generic to allow for + /// different top-level meta objects depending on the associated resource of the request. /// - /// Associated resource for which to build the meta data + /// Associated resource for which to build the meta element. public interface IMetaBuilder where TResource : class, IIdentifiable { /// - /// Adds a key-value pair to the top-level meta data object + /// Adds a key-value pair to the top-level meta object. /// void Add(string key, object value); /// /// Joins the new dictionary with the current one. In the event of a key collision, - /// the new value will override the old. + /// the new value will overwrite the old one. /// - void Add(Dictionary values); + void Add(IReadOnlyDictionary values); /// /// Builds the top-level meta data object. /// - Dictionary GetMeta(); + IDictionary GetMeta(); } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilder.cs new file mode 100644 index 0000000000..4dea18abdb --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilder.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.Building +{ + /// + /// Responsible for converting resources into s + /// given a collection of attributes and relationships. + /// + public interface IResourceObjectBuilder + { + /// + /// Converts into a . + /// Adds the attributes and relationships that are enlisted in and . + /// + /// Resource to build a for. + /// Attributes to include in the building process. + /// Relationships to include in the building process. + /// The resource object that was built. + ResourceObject Build(IIdentifiable resource, IReadOnlyCollection attributes, IReadOnlyCollection relationships); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IResourceObjectBuilderSettingsProvider.cs b/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilderSettingsProvider.cs similarity index 62% rename from src/JsonApiDotNetCore/Serialization/Server/Contracts/IResourceObjectBuilderSettingsProvider.cs rename to src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilderSettingsProvider.cs index 5da12fe37a..5edc8801d4 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IResourceObjectBuilderSettingsProvider.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilderSettingsProvider.cs @@ -1,12 +1,12 @@ -namespace JsonApiDotNetCore.Serialization.Server +namespace JsonApiDotNetCore.Serialization.Building { /// - /// Service that provides the server serializer with + /// Service that provides the server serializer with . /// public interface IResourceObjectBuilderSettingsProvider { /// - /// Gets the behaviour for the serializer it is injected in. + /// Gets the behavior for the serializer it is injected in. /// ResourceObjectBuilderSettings Get(); } diff --git a/src/JsonApiDotNetCore/Serialization/Server/Builders/IncludedResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs similarity index 73% rename from src/JsonApiDotNetCore/Serialization/Server/Builders/IncludedResourceObjectBuilder.cs rename to src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs index 5aafe8a85a..fe25ee0550 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Builders/IncludedResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs @@ -1,14 +1,14 @@ +using System; using System.Collections; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.Server.Builders +namespace JsonApiDotNetCore.Serialization.Building { - /// public class IncludedResourceObjectBuilder : ResourceObjectBuilder, IIncludedResourceObjectBuilder { private readonly HashSet _included; @@ -17,17 +17,17 @@ public class IncludedResourceObjectBuilder : ResourceObjectBuilder, IIncludedRes public IncludedResourceObjectBuilder(IFieldsToSerialize fieldsToSerialize, ILinkBuilder linkBuilder, - IResourceContextProvider provider, + IResourceContextProvider resourceContextProvider, IResourceObjectBuilderSettingsProvider settingsProvider) - : base(provider, settingsProvider.Get()) + : base(resourceContextProvider, settingsProvider.Get()) { - _included = new HashSet(new ResourceObjectComparer()); - _fieldsToSerialize = fieldsToSerialize; - _linkBuilder = linkBuilder; + _included = new HashSet(ResourceIdentifierObjectComparer.Instance); + _fieldsToSerialize = fieldsToSerialize ?? throw new ArgumentNullException(nameof(fieldsToSerialize)); + _linkBuilder = linkBuilder ?? throw new ArgumentNullException(nameof(linkBuilder)); } - /// - public List Build() + /// + public IList Build() { if (_included.Any()) { @@ -42,14 +42,17 @@ public List Build() } resourceObject.Links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); } - return _included.ToList(); + return _included.ToArray(); } return null; } - /// - public void IncludeRelationshipChain(List inclusionChain, IIdentifiable rootResource) + /// + public void IncludeRelationshipChain(IReadOnlyCollection inclusionChain, IIdentifiable rootResource) { + if (inclusionChain == null) throw new ArgumentNullException(nameof(inclusionChain)); + if (rootResource == null) throw new ArgumentNullException(nameof(rootResource)); + // We don't have to build a resource object for the root resource because // this one is already encoded in the documents primary data, so we process the chain // starting from the first related resource. @@ -82,7 +85,7 @@ private void ProcessRelationship(RelationshipAttribute originRelationship, IIden var relationshipsObject = resourceObject.Relationships; // add the relationship entry in the relationship object. if (!relationshipsObject.TryGetValue(nextRelationshipName, out var relationshipEntry)) - relationshipsObject[nextRelationshipName] = (relationshipEntry = GetRelationshipData(nextRelationship, parent)); + relationshipsObject[nextRelationshipName] = relationshipEntry = GetRelationshipData(nextRelationship, parent); relationshipEntry.Data = GetRelatedResourceLinkage(nextRelationship, parent); @@ -93,7 +96,7 @@ private void ProcessRelationship(RelationshipAttribute originRelationship, IIden } } - private List ShiftChain(List chain) + private List ShiftChain(IReadOnlyCollection chain) { var chainRemainder = chain.ToList(); chainRemainder.RemoveAt(0); @@ -101,28 +104,25 @@ private List ShiftChain(List chain } /// - /// We only need a empty relationship object entry here. It will be populated in the + /// We only need an empty relationship object entry here. It will be populated in the /// ProcessRelationships method. /// - /// - /// - /// protected override RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) { + if (relationship == null) throw new ArgumentNullException(nameof(relationship)); + if (resource == null) throw new ArgumentNullException(nameof(resource)); + return new RelationshipEntry { Links = _linkBuilder.GetRelationshipLinks(relationship, resource) }; } /// /// Gets the resource object for by searching the included list. - /// If it was not already build, it is constructed and added to the included list. + /// If it was not already built, it is constructed and added to the inclusion list. /// - /// - /// - /// private ResourceObject GetOrBuildResourceObject(IIdentifiable parent, RelationshipAttribute relationship) { var type = parent.GetType(); - var resourceName = _provider.GetResourceContext(type).ResourceName; + var resourceName = ResourceContextProvider.GetResourceContext(type).ResourceName; var entry = _included.SingleOrDefault(ro => ro.Type == resourceName && ro.Id == parent.StringId); if (entry == null) { diff --git a/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs similarity index 72% rename from src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs rename to src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs index a69291f9f3..b3d58fe84c 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs @@ -3,50 +3,49 @@ using System.Linq; using System.Text; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.QueryStrings; -using JsonApiDotNetCore.RequestServices.Contracts; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Http; -namespace JsonApiDotNetCore.Serialization.Server.Builders +namespace JsonApiDotNetCore.Serialization.Building { public class LinkBuilder : ILinkBuilder { private readonly IResourceContextProvider _provider; private readonly IRequestQueryStringAccessor _queryStringAccessor; private readonly IJsonApiOptions _options; - private readonly ICurrentRequest _currentRequest; + private readonly IJsonApiRequest _request; private readonly IPaginationContext _paginationContext; public LinkBuilder(IJsonApiOptions options, - ICurrentRequest currentRequest, + IJsonApiRequest request, IPaginationContext paginationContext, IResourceContextProvider provider, IRequestQueryStringAccessor queryStringAccessor) { - _options = options; - _currentRequest = currentRequest; - _paginationContext = paginationContext; - _provider = provider; - _queryStringAccessor = queryStringAccessor; + _options = options ?? throw new ArgumentNullException(nameof(options)); + _request = request ?? throw new ArgumentNullException(nameof(request)); + _paginationContext = paginationContext ?? throw new ArgumentNullException(nameof(paginationContext)); + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + _queryStringAccessor = queryStringAccessor ?? throw new ArgumentNullException(nameof(queryStringAccessor)); } - /// + /// public TopLevelLinks GetTopLevelLinks() { - ResourceContext resourceContext = _currentRequest.PrimaryResource; + ResourceContext resourceContext = _request.PrimaryResource; TopLevelLinks topLevelLinks = null; - if (ShouldAddTopLevelLink(resourceContext, Links.Self)) + if (ShouldAddTopLevelLink(resourceContext, LinkTypes.Self)) { topLevelLinks = new TopLevelLinks { Self = GetSelfTopLevelLink(resourceContext) }; } - if (ShouldAddTopLevelLink(resourceContext, Links.Paging) && _paginationContext.PageSize != null) + if (ShouldAddTopLevelLink(resourceContext, LinkTypes.Paging) && _paginationContext.PageSize != null) { SetPageLinks(resourceContext, topLevelLinks ??= new TopLevelLinks()); } @@ -59,9 +58,9 @@ public TopLevelLinks GetTopLevelLinks() /// configuration on the , and if not configured, by checking with the /// global configuration in . /// - private bool ShouldAddTopLevelLink(ResourceContext resourceContext, Links link) + private bool ShouldAddTopLevelLink(ResourceContext resourceContext, LinkTypes link) { - if (resourceContext.TopLevelLinks != Links.NotConfigured) + if (resourceContext.TopLevelLinks != LinkTypes.NotConfigured) { return resourceContext.TopLevelLinks.HasFlag(link); } @@ -92,21 +91,21 @@ private void SetPageLinks(ResourceContext resourceContext, TopLevelLinks links) private string GetSelfTopLevelLink(ResourceContext resourceContext) { var builder = new StringBuilder(); - builder.Append(_currentRequest.BasePath); + builder.Append(_request.BasePath); builder.Append("/"); builder.Append(resourceContext.ResourceName); - string resourceId = _currentRequest.PrimaryId; + string resourceId = _request.PrimaryId; if (resourceId != null) { builder.Append("/"); builder.Append(resourceId); } - if (_currentRequest.Relationship != null) + if (_request.Relationship != null) { builder.Append("/"); - builder.Append(_currentRequest.Relationship.PublicName); + builder.Append(_request.Relationship.PublicName); } builder.Append(DecodeSpecialCharacters(_queryStringAccessor.QueryString.Value)); @@ -122,7 +121,7 @@ private string GetPageLink(ResourceContext resourceContext, int pageOffset, Page parameters["page[number]"] = pageOffset.ToString(); }); - return $"{_currentRequest.BasePath}/{resourceContext.ResourceName}" + queryString; + return $"{_request.BasePath}/{resourceContext.ResourceName}" + queryString; } private string BuildQueryString(Action> updateAction) @@ -139,11 +138,14 @@ private static string DecodeSpecialCharacters(string uri) return uri.Replace("%5B", "[").Replace("%5D", "]").Replace("%27", "'"); } - /// + /// public ResourceLinks GetResourceLinks(string resourceName, string id) { + if (resourceName == null) throw new ArgumentNullException(nameof(resourceName)); + if (id == null) throw new ArgumentNullException(nameof(id)); + var resourceContext = _provider.GetResourceContext(resourceName); - if (ShouldAddResourceLink(resourceContext, Links.Self)) + if (ShouldAddResourceLink(resourceContext, LinkTypes.Self)) { return new ResourceLinks { Self = GetSelfResourceLink(resourceName, id) }; } @@ -151,18 +153,21 @@ public ResourceLinks GetResourceLinks(string resourceName, string id) return null; } - /// + /// public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable parent) { + if (relationship == null) throw new ArgumentNullException(nameof(relationship)); + if (parent == null) throw new ArgumentNullException(nameof(parent)); + var parentResourceContext = _provider.GetResourceContext(parent.GetType()); var childNavigation = relationship.PublicName; RelationshipLinks links = null; - if (ShouldAddRelationshipLink(parentResourceContext, relationship, Links.Related)) + if (ShouldAddRelationshipLink(parentResourceContext, relationship, LinkTypes.Related)) { links = new RelationshipLinks { Related = GetRelatedRelationshipLink(parentResourceContext.ResourceName, parent.StringId, childNavigation) }; } - if (ShouldAddRelationshipLink(parentResourceContext, relationship, Links.Self)) + if (ShouldAddRelationshipLink(parentResourceContext, relationship, LinkTypes.Self)) { links ??= new RelationshipLinks(); links.Self = GetSelfRelationshipLink(parentResourceContext.ResourceName, parent.StringId, childNavigation); @@ -174,17 +179,17 @@ public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship private string GetSelfRelationshipLink(string parent, string parentId, string navigation) { - return $"{_currentRequest.BasePath}/{parent}/{parentId}/relationships/{navigation}"; + return $"{_request.BasePath}/{parent}/{parentId}/relationships/{navigation}"; } private string GetSelfResourceLink(string resource, string resourceId) { - return $"{_currentRequest.BasePath}/{resource}/{resourceId}"; + return $"{_request.BasePath}/{resource}/{resourceId}"; } private string GetRelatedRelationshipLink(string parent, string parentId, string navigation) { - return $"{_currentRequest.BasePath}/{parent}/{parentId}/{navigation}"; + return $"{_request.BasePath}/{parent}/{parentId}/{navigation}"; } /// @@ -192,9 +197,9 @@ private string GetRelatedRelationshipLink(string parent, string parentId, string /// configuration on the , and if not configured, by checking with the /// global configuration in . /// - private bool ShouldAddResourceLink(ResourceContext resourceContext, Links link) + private bool ShouldAddResourceLink(ResourceContext resourceContext, LinkTypes link) { - if (resourceContext.ResourceLinks != Links.NotConfigured) + if (resourceContext.ResourceLinks != LinkTypes.NotConfigured) { return resourceContext.ResourceLinks.HasFlag(link); } @@ -207,13 +212,13 @@ private bool ShouldAddResourceLink(ResourceContext resourceContext, Links link) /// the , and if not configured by checking with the /// global configuration in . /// - private bool ShouldAddRelationshipLink(ResourceContext resourceContext, RelationshipAttribute relationship, Links link) + private bool ShouldAddRelationshipLink(ResourceContext resourceContext, RelationshipAttribute relationship, LinkTypes link) { - if (relationship.RelationshipLinks != Links.NotConfigured) + if (relationship.Links != LinkTypes.NotConfigured) { - return relationship.RelationshipLinks.HasFlag(link); + return relationship.Links.HasFlag(link); } - if (resourceContext.RelationshipLinks != Links.NotConfigured) + if (resourceContext.RelationshipLinks != LinkTypes.NotConfigured) { return resourceContext.RelationshipLinks.HasFlag(link); } diff --git a/src/JsonApiDotNetCore/Serialization/Server/Builders/MetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs similarity index 61% rename from src/JsonApiDotNetCore/Serialization/Server/Builders/MetaBuilder.cs rename to src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs index 9bece409cc..f73bb728b6 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Builders/MetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs @@ -1,14 +1,14 @@ +using System; using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; +using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Resources; -namespace JsonApiDotNetCore.Serialization.Server.Builders +namespace JsonApiDotNetCore.Serialization.Building { - /// - public class MetaBuilder : IMetaBuilder where T : class, IIdentifiable + /// + public class MetaBuilder : IMetaBuilder where TResource : class, IIdentifiable { private Dictionary _meta = new Dictionary(); private readonly IPaginationContext _paginationContext; @@ -17,30 +17,34 @@ public class MetaBuilder : IMetaBuilder where T : class, IIdentifiable private readonly IHasMeta _resourceMeta; public MetaBuilder(IPaginationContext paginationContext, IJsonApiOptions options, IRequestMeta requestMeta = null, - ResourceDefinition resourceDefinition = null) + ResourceDefinition resourceDefinition = null) { - _paginationContext = paginationContext; - _options = options; + _paginationContext = paginationContext ?? throw new ArgumentNullException(nameof(paginationContext)); + _options = options ?? throw new ArgumentNullException(nameof(options)); _requestMeta = requestMeta; _resourceMeta = resourceDefinition as IHasMeta; } - /// + /// public void Add(string key, object value) { - _meta[key] = value; + if (key == null) throw new ArgumentNullException(nameof(key)); + + _meta[key] = value ?? throw new ArgumentNullException(nameof(value)); } - /// - public void Add(Dictionary values) + /// + public void Add(IReadOnlyDictionary values) { + if (values == null) throw new ArgumentNullException(nameof(values)); + _meta = values.Keys.Union(_meta.Keys) .ToDictionary(key => key, key => values.ContainsKey(key) ? values[key] : _meta[key]); } - /// - public Dictionary GetMeta() + /// + public IDictionary GetMeta() { if (_paginationContext.TotalResourceCount != null) { diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResourceIdentifierObjectComparer.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceIdentifierObjectComparer.cs new file mode 100644 index 0000000000..7820f874f0 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Building/ResourceIdentifierObjectComparer.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.Building +{ + internal sealed class ResourceIdentifierObjectComparer : IEqualityComparer + { + public static readonly ResourceIdentifierObjectComparer Instance = new ResourceIdentifierObjectComparer(); + + private ResourceIdentifierObjectComparer() + { + } + + public bool Equals(ResourceIdentifierObject x, ResourceIdentifierObject y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (x is null || y is null || x.GetType() != y.GetType()) + { + return false; + } + + return x.Id == y.Id && x.Type == y.Type; + } + + public int GetHashCode(ResourceIdentifierObject obj) + { + return obj.GetHashCode(); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs similarity index 71% rename from src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilder.cs rename to src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs index 0ecbccbd3f..31c9f2555d 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs @@ -2,38 +2,38 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; using Newtonsoft.Json; -namespace JsonApiDotNetCore.Serialization +namespace JsonApiDotNetCore.Serialization.Building { - - /// + /// public class ResourceObjectBuilder : IResourceObjectBuilder { - protected readonly IResourceContextProvider _provider; + protected IResourceContextProvider ResourceContextProvider { get; } private readonly ResourceObjectBuilderSettings _settings; - private const string _identifiablePropertyName = nameof(Identifiable.Id); - public ResourceObjectBuilder(IResourceContextProvider provider, ResourceObjectBuilderSettings settings) + public ResourceObjectBuilder(IResourceContextProvider resourceContextProvider, ResourceObjectBuilderSettings settings) { - _provider = provider; - _settings = settings; + ResourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); } - /// - public ResourceObject Build(IIdentifiable resource, IEnumerable attributes = null, IEnumerable relationships = null) + /// + public ResourceObject Build(IIdentifiable resource, IReadOnlyCollection attributes = null, IReadOnlyCollection relationships = null) { - var resourceContext = _provider.GetResourceContext(resource.GetType()); + if (resource == null) throw new ArgumentNullException(nameof(resource)); + + var resourceContext = ResourceContextProvider.GetResourceContext(resource.GetType()); // populating the top-level "type" and "id" members. var ro = new ResourceObject { Type = resourceContext.ResourceName, Id = resource.StringId == string.Empty ? null : resource.StringId }; // populating the top-level "attribute" member of a resource object. never include "id" as an attribute - if (attributes != null && (attributes = attributes.Where(attr => attr.Property.Name != _identifiablePropertyName)).Any()) + if (attributes != null && (attributes = attributes.Where(attr => attr.Property.Name != nameof(Identifiable.Id)).ToArray()).Any()) ProcessAttributes(resource, attributes, ro); // populating the top-level "relationship" member of a resource object. @@ -45,13 +45,16 @@ public ResourceObject Build(IIdentifiable resource, IEnumerable a /// /// Builds the entries of the "relationships - /// objects" The default behaviour is to just construct a resource linkage + /// objects". The default behavior is to just construct a resource linkage /// with the "data" field populated with "single" or "many" data. /// Depending on the requirements of the implementation (server or client serializer), /// this may be overridden. /// protected virtual RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) { + if (relationship == null) throw new ArgumentNullException(nameof(relationship)); + if (resource == null) throw new ArgumentNullException(nameof(resource)); + return new RelationshipEntry { Data = GetRelatedResourceLinkage(relationship, resource) }; } @@ -60,16 +63,18 @@ protected virtual RelationshipEntry GetRelationshipData(RelationshipAttribute re /// protected object GetRelatedResourceLinkage(RelationshipAttribute relationship, IIdentifiable resource) { - if (relationship is HasOneAttribute hasOne) - return GetRelatedResourceLinkage(hasOne, resource); + if (relationship == null) throw new ArgumentNullException(nameof(relationship)); + if (resource == null) throw new ArgumentNullException(nameof(resource)); - return GetRelatedResourceLinkage((HasManyAttribute)relationship, resource); + return relationship is HasOneAttribute hasOne + ? (object) GetRelatedResourceLinkageForHasOne(hasOne, resource) + : GetRelatedResourceLinkageForHasMany((HasManyAttribute) relationship, resource); } /// - /// Builds a for a HasOne relationship + /// Builds a for a HasOne relationship. /// - private ResourceIdentifierObject GetRelatedResourceLinkage(HasOneAttribute relationship, IIdentifiable resource) + private ResourceIdentifierObject GetRelatedResourceLinkageForHasOne(HasOneAttribute relationship, IIdentifiable resource) { var relatedResource = (IIdentifiable)relationship.GetValue(resource); if (relatedResource == null && IsRequiredToOneRelationship(relationship, resource)) @@ -82,9 +87,9 @@ private ResourceIdentifierObject GetRelatedResourceLinkage(HasOneAttribute relat } /// - /// Builds the s for a HasMany relationship + /// Builds the s for a HasMany relationship. /// - private List GetRelatedResourceLinkage(HasManyAttribute relationship, IIdentifiable resource) + private List GetRelatedResourceLinkageForHasMany(HasManyAttribute relationship, IIdentifiable resource) { var relatedResources = (IEnumerable)relationship.GetValue(resource); var manyData = new List(); @@ -100,7 +105,7 @@ private List GetRelatedResourceLinkage(HasManyAttribut /// private ResourceIdentifierObject GetResourceIdentifier(IIdentifiable resource) { - var resourceName = _provider.GetResourceContext(resource.GetType()).ResourceName; + var resourceName = ResourceContextProvider.GetResourceContext(resource.GetType()).ResourceName; return new ResourceIdentifierObject { Type = resourceName, @@ -124,7 +129,7 @@ private bool IsRequiredToOneRelationship(HasOneAttribute attr, IIdentifiable res /// Puts the relationships of the resource into the resource object. /// private void ProcessRelationships(IIdentifiable resource, IEnumerable relationships, ResourceObject ro) - { + { foreach (var rel in relationships) { var relData = GetRelationshipData(rel, resource); @@ -148,7 +153,7 @@ private void ProcessAttributes(IIdentifiable resource, IEnumerable /// Options used to configure how fields of a model get serialized into diff --git a/src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs similarity index 68% rename from src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs rename to src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs index 4375c3d59b..8b317c3e3e 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs @@ -1,10 +1,11 @@ +using System; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.QueryStrings; -namespace JsonApiDotNetCore.Serialization.Server +namespace JsonApiDotNetCore.Serialization.Building { /// - /// This implementation of the behaviour provider reads the query params that + /// This implementation of the behavior provider reads the defaults/nulls query string parameters that /// can, if provided, override the settings in . /// public sealed class ResourceObjectBuilderSettingsProvider : IResourceObjectBuilderSettingsProvider @@ -14,11 +15,11 @@ public sealed class ResourceObjectBuilderSettingsProvider : IResourceObjectBuild public ResourceObjectBuilderSettingsProvider(IDefaultsQueryStringParameterReader defaultsReader, INullsQueryStringParameterReader nullsReader) { - _defaultsReader = defaultsReader; - _nullsReader = nullsReader; + _defaultsReader = defaultsReader ?? throw new ArgumentNullException(nameof(defaultsReader)); + _nullsReader = nullsReader ?? throw new ArgumentNullException(nameof(nullsReader)); } - /// + /// public ResourceObjectBuilderSettings Get() { return new ResourceObjectBuilderSettings(_nullsReader.SerializerNullValueHandling, _defaultsReader.SerializerDefaultValueHandling); diff --git a/src/JsonApiDotNetCore/Serialization/Server/Builders/ResponseResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs similarity index 71% rename from src/JsonApiDotNetCore/Serialization/Server/Builders/ResponseResourceObjectBuilder.cs rename to src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs index 977570345f..e7a1344b53 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Builders/ResponseResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs @@ -1,14 +1,15 @@ +using System; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.QueryStrings; -using JsonApiDotNetCore.Serialization.Server.Builders; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.Server +namespace JsonApiDotNetCore.Serialization.Building { public class ResponseResourceObjectBuilder : ResourceObjectBuilder { @@ -20,18 +21,20 @@ public class ResponseResourceObjectBuilder : ResourceObjectBuilder public ResponseResourceObjectBuilder(ILinkBuilder linkBuilder, IIncludedResourceObjectBuilder includedBuilder, IEnumerable constraintProviders, - IResourceContextProvider provider, + IResourceContextProvider resourceContextProvider, IResourceObjectBuilderSettingsProvider settingsProvider) - : base(provider, settingsProvider.Get()) + : base(resourceContextProvider, settingsProvider.Get()) { - _linkBuilder = linkBuilder; - _includedBuilder = includedBuilder; - _constraintProviders = constraintProviders; + _linkBuilder = linkBuilder ?? throw new ArgumentNullException(nameof(linkBuilder)); + _includedBuilder = includedBuilder ?? throw new ArgumentNullException(nameof(includedBuilder)); + _constraintProviders = constraintProviders ?? throw new ArgumentNullException(nameof(constraintProviders)); } public RelationshipEntry Build(IIdentifiable resource, RelationshipAttribute requestRelationship) { - _requestRelationship = requestRelationship; + if (resource == null) throw new ArgumentNullException(nameof(resource)); + + _requestRelationship = requestRelationship ?? throw new ArgumentNullException(nameof(requestRelationship)); return GetRelationshipData(requestRelationship, resource); } @@ -44,8 +47,11 @@ public RelationshipEntry Build(IIdentifiable resource, RelationshipAttribute req /// protected override RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) { + if (relationship == null) throw new ArgumentNullException(nameof(relationship)); + if (resource == null) throw new ArgumentNullException(nameof(resource)); + RelationshipEntry relationshipEntry = null; - List> relationshipChains = null; + List> relationshipChains = null; if (Equals(relationship, _requestRelationship) || ShouldInclude(relationship, out relationshipChains)) { relationshipEntry = base.GetRelationshipData(relationship, resource); @@ -69,21 +75,22 @@ protected override RelationshipEntry GetRelationshipData(RelationshipAttribute r /// Inspects the included relationship chains (see /// to see if should be included or not. /// - private bool ShouldInclude(RelationshipAttribute relationship, out List> inclusionChain) + private bool ShouldInclude(RelationshipAttribute relationship, out List> inclusionChain) { - var includes = _constraintProviders + var chains = _constraintProviders .SelectMany(p => p.GetConstraints()) .Select(expressionInScope => expressionInScope.Expression) .OfType() + .SelectMany(IncludeChainConverter.GetRelationshipChains) .ToArray(); - inclusionChain = new List>(); + inclusionChain = new List>(); - foreach (var chain in includes.SelectMany(IncludeChainConverter.GetRelationshipChains)) + foreach (var chain in chains) { if (chain.Fields.First().Equals(relationship)) { - inclusionChain.Add(chain.Fields.Cast().ToList()); + inclusionChain.Add(chain.Fields.Cast().ToArray()); } } diff --git a/src/JsonApiDotNetCore/Serialization/Client/DeserializedResponse.cs b/src/JsonApiDotNetCore/Serialization/Client/DeserializedResponse.cs deleted file mode 100644 index be872d5955..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Client/DeserializedResponse.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; - -namespace JsonApiDotNetCore.Serialization.Client -{ - /// Base class for "single data" and "many data" deserialized responses. - /// TODO: Currently and - /// information is ignored by the serializer. This is out of scope for now because - /// it is not considered mission critical for v4. - public abstract class DeserializedResponseBase - { - public TopLevelLinks Links { get; set; } - public Dictionary Meta { get; set; } - public object Errors { get; set; } - public object JsonApi { get; set; } - } - - /// - /// Represents a deserialized document with "single data". - /// - /// Type of the resource in the primary data - public sealed class DeserializedSingleResponse : DeserializedResponseBase where TResource : class, IIdentifiable - { - public TResource Data { get; set; } - } - - /// - /// Represents a deserialized document with "many data". - /// - /// Type of the resource(s) in the primary data - public sealed class DeserializedListResponse : DeserializedResponseBase where TResource : class, IIdentifiable - { - public List Data { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Client/IResponseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Client/IResponseDeserializer.cs deleted file mode 100644 index c40a07e8a2..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Client/IResponseDeserializer.cs +++ /dev/null @@ -1,26 +0,0 @@ -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCore.Serialization.Client -{ - /// - /// Client deserializer. Currently not used internally in JsonApiDotNetCore, - /// except for in the tests. Exposed publicly to make testing easier or to implement - /// server-to-server communication. - /// - public interface IResponseDeserializer - { - /// - /// Deserializes a response with a single resource (or null) as data. - /// - /// The type of the resources in the primary data - /// The JSON to be deserialized - DeserializedSingleResponse DeserializeSingle(string body) where TResource : class, IIdentifiable; - - /// - /// Deserializes a response with a (empty) list of resources as data. - /// - /// The type of the resources in the primary data - /// The JSON to be deserialized - DeserializedListResponse DeserializeList(string body) where TResource : class, IIdentifiable; - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/DeserializedResponseBase.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/DeserializedResponseBase.cs new file mode 100644 index 0000000000..c2831e327e --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/DeserializedResponseBase.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.Client.Internal +{ + /// Base class for "single data" and "many data" deserialized responses. + /// TODO: Currently and + /// information is ignored by the serializer. This is out of scope for now because + /// it is not considered mission critical for v4. + public abstract class DeserializedResponseBase + { + public TopLevelLinks Links { get; set; } + public IDictionary Meta { get; set; } + public object Errors { get; set; } + public object JsonApi { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Client/IRequestSerializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/IRequestSerializer.cs similarity index 67% rename from src/JsonApiDotNetCore/Serialization/Client/IRequestSerializer.cs rename to src/JsonApiDotNetCore/Serialization/Client/Internal/IRequestSerializer.cs index fa53d1bf5a..eabca7e593 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/IRequestSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/IRequestSerializer.cs @@ -1,12 +1,12 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Serialization.Client +namespace JsonApiDotNetCore.Serialization.Client.Internal { /// - /// Interface for client serializer that can be used to register with the DI, for usage in + /// Interface for client serializer that can be used to register with the DI container, for usage in /// custom services or repositories. /// public interface IRequestSerializer @@ -18,23 +18,23 @@ public interface IRequestSerializer string Serialize(IIdentifiable resource); /// - /// Creates and serializes a document for a list of resources. + /// Creates and serializes a document for a collection of resources. /// /// The serialized content - string Serialize(IEnumerable resources); + string Serialize(IReadOnlyCollection resources); /// /// Sets the attributes that will be included in the serialized payload. /// You can use /// to conveniently access the desired instances. /// - public IEnumerable AttributesToSerialize { set; } + public IReadOnlyCollection AttributesToSerialize { set; } /// /// Sets the relationships that will be included in the serialized payload. /// You can use /// to conveniently access the desired instances. /// - public IEnumerable RelationshipsToSerialize { set; } + public IReadOnlyCollection RelationshipsToSerialize { set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/IResponseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/IResponseDeserializer.cs new file mode 100644 index 0000000000..c4ede45737 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/IResponseDeserializer.cs @@ -0,0 +1,26 @@ +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Serialization.Client.Internal +{ + /// + /// Client deserializer. Currently not used internally in JsonApiDotNetCore, + /// except for in the tests. Exposed publicly to make testing easier or to implement + /// server-to-server communication. + /// + public interface IResponseDeserializer + { + /// + /// Deserializes a response with a single resource (or null) as data. + /// + /// The type of the resources in the primary data. + /// The JSON to be deserialized. + SingleResponse DeserializeSingle(string body) where TResource : class, IIdentifiable; + + /// + /// Deserializes a response with an (empty) collection of resources as data. + /// + /// The type of the resources in the primary data. + /// The JSON to be deserialized. + ManyResponse DeserializeMany(string body) where TResource : class, IIdentifiable; + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/ManyResponse.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/ManyResponse.cs new file mode 100644 index 0000000000..954b65e167 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/ManyResponse.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Serialization.Client.Internal +{ + /// + /// Represents a deserialized document with "many data". + /// + /// Type of the resource(s) in the primary data. + public sealed class ManyResponse : DeserializedResponseBase where TResource : class, IIdentifiable + { + public IReadOnlyCollection Data { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Client/RequestSerializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs similarity index 58% rename from src/JsonApiDotNetCore/Serialization/Client/RequestSerializer.cs rename to src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs index 23d981d43c..2e0cf5f461 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/RequestSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs @@ -1,17 +1,19 @@ using System; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Building; +using JsonApiDotNetCore.Serialization.Objects; using Newtonsoft.Json; -namespace JsonApiDotNetCore.Serialization.Client +namespace JsonApiDotNetCore.Serialization.Client.Internal { /// - /// Client serializer implementation of + /// Client serializer implementation of . /// - public class RequestSerializer : BaseDocumentBuilder, IRequestSerializer + public class RequestSerializer : BaseSerializer, IRequestSerializer { private Type _currentTargetedResource; private readonly IResourceGraph _resourceGraph; @@ -21,10 +23,10 @@ public RequestSerializer(IResourceGraph resourceGraph, IResourceObjectBuilder resourceObjectBuilder) : base(resourceObjectBuilder) { - _resourceGraph = resourceGraph; + _resourceGraph = resourceGraph ?? throw new ArgumentNullException(nameof(resourceGraph)); } - /// + /// public string Serialize(IIdentifiable resource) { if (resource == null) @@ -40,42 +42,43 @@ public string Serialize(IIdentifiable resource) return SerializeObject(document, _jsonSerializerSettings); } - /// - public string Serialize(IEnumerable resources) + /// + public string Serialize(IReadOnlyCollection resources) { - IIdentifiable resource = null; - foreach (IIdentifiable item in resources) + if (resources == null) throw new ArgumentNullException(nameof(resources)); + + IIdentifiable firstResource = resources.FirstOrDefault(); + + Document document; + if (firstResource == null) { - resource = item; - break; + document = Build(resources, Array.Empty(), Array.Empty()); } - - if (resource == null) + else { - var result = Build(resources, Array.Empty(), Array.Empty()); - return SerializeObject(result, _jsonSerializerSettings); + _currentTargetedResource = firstResource.GetType(); + var attributes = GetAttributesToSerialize(firstResource); + var relationships = GetRelationshipsToSerialize(firstResource); + + document = Build(resources, attributes, relationships); + _currentTargetedResource = null; } - _currentTargetedResource = resource.GetType(); - var attributes = GetAttributesToSerialize(resource); - var relationships = GetRelationshipsToSerialize(resource); - var document = Build(resources, attributes, relationships); - _currentTargetedResource = null; return SerializeObject(document, _jsonSerializerSettings); } - /// - public IEnumerable AttributesToSerialize { private get; set; } + /// + public IReadOnlyCollection AttributesToSerialize { private get; set; } - /// - public IEnumerable RelationshipsToSerialize { private get; set; } + /// + public IReadOnlyCollection RelationshipsToSerialize { private get; set; } /// /// By default, the client serializer includes all attributes in the result, /// unless a list of allowed attributes was supplied using the /// method. For any related resources, attributes are never exposed. /// - private List GetAttributesToSerialize(IIdentifiable resource) + private IReadOnlyCollection GetAttributesToSerialize(IIdentifiable resource) { var currentResourceType = resource.GetType(); if (_currentTargetedResource != currentResourceType) @@ -86,7 +89,7 @@ private List GetAttributesToSerialize(IIdentifiable resource) if (AttributesToSerialize == null) return _resourceGraph.GetAttributes(currentResourceType); - return AttributesToSerialize.ToList(); + return AttributesToSerialize; } /// @@ -94,16 +97,16 @@ private List GetAttributesToSerialize(IIdentifiable resource) /// for resources in the primary data unless explicitly included using /// . /// - private List GetRelationshipsToSerialize(IIdentifiable resource) + private IReadOnlyCollection GetRelationshipsToSerialize(IIdentifiable resource) { var currentResourceType = resource.GetType(); // only allow relationship attributes to be serialized if they were set using // - // and the current is a primary entry. + // and the current resource is a primary entry. if (RelationshipsToSerialize == null) return _resourceGraph.GetRelationships(currentResourceType); - return RelationshipsToSerialize.ToList(); + return RelationshipsToSerialize; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Client/ResponseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/ResponseDeserializer.cs similarity index 59% rename from src/JsonApiDotNetCore/Serialization/Client/ResponseDeserializer.cs rename to src/JsonApiDotNetCore/Serialization/Client/Internal/ResponseDeserializer.cs index edf42d7ce4..277cd0b2c0 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/ResponseDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/ResponseDeserializer.cs @@ -1,44 +1,47 @@ using System; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.Client +namespace JsonApiDotNetCore.Serialization.Client.Internal { /// - /// Client deserializer implementation of the + /// Client deserializer implementation of the . /// - public class ResponseDeserializer : BaseDocumentParser, IResponseDeserializer + public class ResponseDeserializer : BaseDeserializer, IResponseDeserializer { - public ResponseDeserializer(IResourceContextProvider contextProvider, IResourceFactory resourceFactory) : base(contextProvider, resourceFactory) { } + public ResponseDeserializer(IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory) : base(resourceContextProvider, resourceFactory) { } - /// - public DeserializedSingleResponse DeserializeSingle(string body) where TResource : class, IIdentifiable + /// + public SingleResponse DeserializeSingle(string body) where TResource : class, IIdentifiable { - var resource = Deserialize(body); - return new DeserializedSingleResponse + if (body == null) throw new ArgumentNullException(nameof(body)); + + var resource = DeserializeBody(body); + return new SingleResponse { - Links = _document.Links, - Meta = _document.Meta, + Links = Document.Links, + Meta = Document.Meta, Data = (TResource) resource, JsonApi = null, Errors = null }; } - /// - public DeserializedListResponse DeserializeList(string body) where TResource : class, IIdentifiable + /// + public ManyResponse DeserializeMany(string body) where TResource : class, IIdentifiable { - var resources = Deserialize(body); - return new DeserializedListResponse + if (body == null) throw new ArgumentNullException(nameof(body)); + + var resources = DeserializeBody(body); + return new ManyResponse { - Links = _document.Links, - Meta = _document.Meta, - Data = ((ICollection) resources)?.Cast().ToList(), + Links = Document.Links, + Meta = Document.Meta, + Data = ((ICollection) resources)?.Cast().ToArray(), JsonApi = null, Errors = null }; @@ -49,46 +52,49 @@ public DeserializedListResponse DeserializeList(string bod /// for parsing the property. When a relationship value is parsed, /// it goes through the included list to set its attributes and relationships. /// - /// The resource that was constructed from the document's body - /// The metadata for the exposed field - /// Relationship data for . Is null when is not a + /// The resource that was constructed from the document's body. + /// The metadata for the exposed field. + /// Relationship data for . Is null when is not a . protected override void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipEntry data = null) { + if (resource == null) throw new ArgumentNullException(nameof(resource)); + if (field == null) throw new ArgumentNullException(nameof(field)); + // Client deserializers do not need additional processing for attributes. if (field is AttrAttribute) return; // if the included property is empty or absent, there is no additional data to be parsed. - if (_document.Included == null || _document.Included.Count == 0) + if (Document.Included == null || Document.Included.Count == 0) return; if (field is HasOneAttribute hasOneAttr) { // add attributes and relationships of a parsed HasOne relationship var rio = data.SingleData; - hasOneAttr.SetValue(resource, rio == null ? null : ParseIncludedRelationship(hasOneAttr, rio), _resourceFactory); + hasOneAttr.SetValue(resource, rio == null ? null : ParseIncludedRelationship(hasOneAttr, rio), ResourceFactory); } else if (field is HasManyAttribute hasManyAttr) { // add attributes and relationships of a parsed HasMany relationship var items = data.ManyData.Select(rio => ParseIncludedRelationship(hasManyAttr, rio)); - var values = items.CopyToTypedCollection(hasManyAttr.Property.PropertyType); - hasManyAttr.SetValue(resource, values, _resourceFactory); + var values = TypeHelper.CopyToTypedCollection(items, hasManyAttr.Property.PropertyType); + hasManyAttr.SetValue(resource, values, ResourceFactory); } } /// - /// Searches for and parses the included relationship + /// Searches for and parses the included relationship. /// private IIdentifiable ParseIncludedRelationship(RelationshipAttribute relationshipAttr, ResourceIdentifierObject relatedResourceIdentifier) { - var relatedInstance = (IIdentifiable)_resourceFactory.CreateInstance(relationshipAttr.RightType); + var relatedInstance = (IIdentifiable)ResourceFactory.CreateInstance(relationshipAttr.RightType); relatedInstance.StringId = relatedResourceIdentifier.Id; var includedResource = GetLinkedResource(relatedResourceIdentifier); if (includedResource == null) return relatedInstance; - var resourceContext = _contextProvider.GetResourceContext(relatedResourceIdentifier.Type); + var resourceContext = ResourceContextProvider.GetResourceContext(relatedResourceIdentifier.Type); if (resourceContext == null) throw new InvalidOperationException($"Included type '{relationshipAttr.RightType}' is not a registered json:api resource."); @@ -101,11 +107,11 @@ private ResourceObject GetLinkedResource(ResourceIdentifierObject relatedResourc { try { - return _document.Included.SingleOrDefault(r => r.Type == relatedResourceIdentifier.Type && r.Id == relatedResourceIdentifier.Id); + return Document.Included.SingleOrDefault(r => r.Type == relatedResourceIdentifier.Type && r.Id == relatedResourceIdentifier.Id); } catch (InvalidOperationException e) { - throw new InvalidOperationException("A compound document MUST NOT include more than one resource object for each type and id pair." + throw new InvalidOperationException("A compound document MUST NOT include more than one resource object for each type and ID pair." + $"The duplicate pair was '{relatedResourceIdentifier.Type}, {relatedResourceIdentifier.Id}'", e); } } diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/SingleResponse.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/SingleResponse.cs new file mode 100644 index 0000000000..562317f2bf --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/SingleResponse.cs @@ -0,0 +1,13 @@ +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Serialization.Client.Internal +{ + /// + /// Represents a deserialized document with "single data". + /// + /// Type of the resource in the primary data. + public sealed class SingleResponse : DeserializedResponseBase where TResource : class, IIdentifiable + { + public TResource Data { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Common/IResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Common/IResourceObjectBuilder.cs deleted file mode 100644 index 31aec7ba6c..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Common/IResourceObjectBuilder.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; - -namespace JsonApiDotNetCore.Serialization -{ - /// - /// Responsible for converting resources into s - /// given a list of attributes and relationships. - /// - public interface IResourceObjectBuilder - { - /// - /// Converts into a . - /// Adds the attributes and relationships that are enlisted in and - /// - /// Resource to build a Resource Object for - /// Attributes to include in the building process - /// Relationships to include in the building process - /// The resource object that was built - ResourceObject Build(IIdentifiable resource, IEnumerable attributes, IEnumerable relationships); - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Server/FieldsToSerialize.cs b/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs similarity index 81% rename from src/JsonApiDotNetCore/Serialization/Server/FieldsToSerialize.cs rename to src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs index ccc3a4e4fc..5f5a113430 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/FieldsToSerialize.cs +++ b/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs @@ -1,16 +1,15 @@ -using JsonApiDotNetCore.Internal.Contracts; using System; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.Services.Contract; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Serialization.Server +namespace JsonApiDotNetCore.Serialization { - /// + /// public class FieldsToSerialize : IFieldsToSerialize { private readonly IResourceGraph _resourceGraph; @@ -22,14 +21,16 @@ public FieldsToSerialize( IEnumerable constraintProviders, IResourceDefinitionProvider resourceDefinitionProvider) { - _resourceGraph = resourceGraph; - _constraintProviders = constraintProviders; - _resourceDefinitionProvider = resourceDefinitionProvider; + _resourceGraph = resourceGraph ?? throw new ArgumentNullException(nameof(resourceGraph)); + _constraintProviders = constraintProviders ?? throw new ArgumentNullException(nameof(constraintProviders)); + _resourceDefinitionProvider = resourceDefinitionProvider ?? throw new ArgumentNullException(nameof(resourceDefinitionProvider)); } - /// + /// public IReadOnlyCollection GetAttributes(Type resourceType, RelationshipAttribute relationship = null) - { + { + if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + var sparseFieldSetAttributes = _constraintProviders .SelectMany(p => p.GetConstraints()) .Where(expressionInScope => relationship == null @@ -71,7 +72,7 @@ private HashSet GetViewableAttributes(Type resourceType) .ToHashSet(); } - /// + /// /// /// Note: this method does NOT check if a relationship is included to determine /// if it should be serialized. This is because completely hiding a relationship @@ -80,6 +81,8 @@ private HashSet GetViewableAttributes(Type resourceType) /// public IReadOnlyCollection GetRelationships(Type type) { + if (type == null) throw new ArgumentNullException(nameof(type)); + return _resourceGraph.GetRelationships(type); } } diff --git a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IFieldsToSerialize.cs b/src/JsonApiDotNetCore/Serialization/IFieldsToSerialize.cs similarity index 54% rename from src/JsonApiDotNetCore/Serialization/Server/Contracts/IFieldsToSerialize.cs rename to src/JsonApiDotNetCore/Serialization/IFieldsToSerialize.cs index 490732bbc6..0c41e74abd 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IFieldsToSerialize.cs +++ b/src/JsonApiDotNetCore/Serialization/IFieldsToSerialize.cs @@ -1,28 +1,25 @@ using System; using System.Collections.Generic; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Serialization.Server +namespace JsonApiDotNetCore.Serialization { /// /// Responsible for getting the set of fields that are to be included for a /// given type in the serialization result. Typically combines various sources - /// of information, like application-wide hidden fields as set in - /// , or request-wide hidden fields - /// through sparse field selection. + /// of information, like application-wide and request-wide sparse fieldsets. /// public interface IFieldsToSerialize { /// - /// Gets the list of attributes that are to be serialized for resource of type . - /// If is non-null, it will consider the allowed list of attributes + /// Gets the collection of attributes that are to be serialized for resources of type . + /// If is non-null, it will consider the allowed collection of attributes /// as an included relationship. /// IReadOnlyCollection GetAttributes(Type resourceType, RelationshipAttribute relationship = null); /// - /// Gets the list of relationships that are to be serialized for resource of type . + /// Gets the collection of relationships that are to be serialized for resources of type . /// IReadOnlyCollection GetRelationships(Type type); } diff --git a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiDeserializer.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiDeserializer.cs similarity index 50% rename from src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiDeserializer.cs rename to src/JsonApiDotNetCore/Serialization/IJsonApiDeserializer.cs index 7edc7f897a..eeebb47d95 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiDeserializer.cs @@ -1,6 +1,6 @@ -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Serialization.Server +namespace JsonApiDotNetCore.Serialization { /// /// Deserializer used internally in JsonApiDotNetCore to deserialize requests. @@ -8,11 +8,11 @@ namespace JsonApiDotNetCore.Serialization.Server public interface IJsonApiDeserializer { /// - /// Deserializes JSON in to a and constructs resources + /// Deserializes JSON into a and constructs resources /// from . /// - /// The JSON to be deserialized - /// The resources constructed from the content + /// The JSON to be deserialized. + /// The resources constructed from the content. object Deserialize(string body); } } diff --git a/src/JsonApiDotNetCore/Formatters/IJsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs similarity index 62% rename from src/JsonApiDotNetCore/Formatters/IJsonApiReader.cs rename to src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs index 948a0eac03..37fc344c0e 100644 --- a/src/JsonApiDotNetCore/Formatters/IJsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs @@ -1,11 +1,11 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Formatters; -namespace JsonApiDotNetCore.Formatters +namespace JsonApiDotNetCore.Serialization { /// - /// The deserializer of the body, used in .NET core internally - /// to process `FromBody` + /// The deserializer of the body, used in ASP.NET Core internally + /// to process `FromBody`. /// public interface IJsonApiReader { diff --git a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiSerializer.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs similarity index 69% rename from src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiSerializer.cs rename to src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs index b745f499cb..29d09a2c36 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs @@ -1,4 +1,4 @@ -namespace JsonApiDotNetCore.Serialization.Server +namespace JsonApiDotNetCore.Serialization { /// /// Serializer used internally in JsonApiDotNetCore to serialize responses. @@ -6,7 +6,7 @@ namespace JsonApiDotNetCore.Serialization.Server public interface IJsonApiSerializer { /// - /// Serializes a single resource or a list of resources. + /// Serializes a single resource or a collection of resources. /// string Serialize(object content); } diff --git a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiSerializerFactory.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiSerializerFactory.cs similarity index 81% rename from src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiSerializerFactory.cs rename to src/JsonApiDotNetCore/Serialization/IJsonApiSerializerFactory.cs index def323437a..75d7f928af 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiSerializerFactory.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiSerializerFactory.cs @@ -1,4 +1,4 @@ -namespace JsonApiDotNetCore.Serialization.Server +namespace JsonApiDotNetCore.Serialization { public interface IJsonApiSerializerFactory { diff --git a/src/JsonApiDotNetCore/Formatters/IJsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiWriter.cs similarity index 81% rename from src/JsonApiDotNetCore/Formatters/IJsonApiWriter.cs rename to src/JsonApiDotNetCore/Serialization/IJsonApiWriter.cs index ce8b7da6a4..0f8287801a 100644 --- a/src/JsonApiDotNetCore/Formatters/IJsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiWriter.cs @@ -1,7 +1,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Formatters; -namespace JsonApiDotNetCore.Formatters +namespace JsonApiDotNetCore.Serialization { public interface IJsonApiWriter { diff --git a/src/JsonApiDotNetCore/Services/Contract/IRequestMeta.cs b/src/JsonApiDotNetCore/Serialization/IRequestMeta.cs similarity index 65% rename from src/JsonApiDotNetCore/Services/Contract/IRequestMeta.cs rename to src/JsonApiDotNetCore/Serialization/IRequestMeta.cs index 3083acdfe1..6fa34c0d9a 100644 --- a/src/JsonApiDotNetCore/Services/Contract/IRequestMeta.cs +++ b/src/JsonApiDotNetCore/Serialization/IRequestMeta.cs @@ -1,7 +1,8 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; -namespace JsonApiDotNetCore.Services +namespace JsonApiDotNetCore.Serialization { /// /// Service to add global top-level metadata to a . @@ -10,6 +11,6 @@ namespace JsonApiDotNetCore.Services /// public interface IRequestMeta { - Dictionary GetMeta(); + IReadOnlyDictionary GetMeta(); } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/IResponseSerializer.cs similarity index 76% rename from src/JsonApiDotNetCore/Serialization/Server/Contracts/IResponseSerializer.cs rename to src/JsonApiDotNetCore/Serialization/IResponseSerializer.cs index 06c9946898..31635d5e36 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/IResponseSerializer.cs @@ -1,6 +1,6 @@ -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Serialization.Server +namespace JsonApiDotNetCore.Serialization { internal interface IResponseSerializer { diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs similarity index 74% rename from src/JsonApiDotNetCore/Formatters/JsonApiReader.cs rename to src/JsonApiDotNetCore/Serialization/JsonApiReader.cs index d6b7621755..12e8c95bd0 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs @@ -2,30 +2,32 @@ using System.Collections; using System.IO; using System.Threading.Tasks; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.RequestServices.Contracts; -using JsonApiDotNetCore.Serialization.Server; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.Logging; -namespace JsonApiDotNetCore.Formatters +namespace JsonApiDotNetCore.Serialization { /// public class JsonApiReader : IJsonApiReader { private readonly IJsonApiDeserializer _deserializer; - private readonly ICurrentRequest _currentRequest; - private readonly ILogger _logger; + private readonly IJsonApiRequest _request; + private readonly TraceLogWriter _traceWriter; public JsonApiReader(IJsonApiDeserializer deserializer, - ICurrentRequest currentRequest, + IJsonApiRequest request, ILoggerFactory loggerFactory) { - _deserializer = deserializer; - _currentRequest = currentRequest; - _logger = loggerFactory.CreateLogger(); + if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); + + _deserializer = deserializer ?? throw new ArgumentNullException(nameof(deserializer)); + _request = request ?? throw new ArgumentNullException(nameof(request)); + _traceWriter = new TraceLogWriter(loggerFactory); } public async Task ReadAsync(InputFormatterContext context) @@ -42,7 +44,7 @@ public async Task ReadAsync(InputFormatterContext context) string body = await GetRequestBody(context.HttpContext.Request.Body); string url = context.HttpContext.Request.GetEncodedUrl(); - _logger.LogTrace($"Received request at '{url}' with body: <<{body}>>"); + _traceWriter.LogMessage(() => $"Received request at '{url}' with body: <<{body}>>"); object model; try @@ -59,21 +61,26 @@ public async Task ReadAsync(InputFormatterContext context) throw new InvalidRequestBodyException(null, null, body, exception); } + ValidatePatchRequestIncludesId(context, model, body); + + return await InputFormatterResult.SuccessAsync(model); + } + + private void ValidatePatchRequestIncludesId(InputFormatterContext context, object model, string body) + { if (context.HttpContext.Request.Method == "PATCH") { bool hasMissingId = model is IList list ? HasMissingId(list) : HasMissingId(model); if (hasMissingId) { - throw new InvalidRequestBodyException("Payload must include id attribute.", null, body); + throw new InvalidRequestBodyException("Payload must include 'id' element.", null, body); } - if (_currentRequest.Kind == EndpointKind.Primary && TryGetId(model, out var bodyId) && bodyId != _currentRequest.PrimaryId) + if (_request.Kind == EndpointKind.Primary && TryGetId(model, out var bodyId) && bodyId != _request.PrimaryId) { - throw new ResourceIdMismatchException(bodyId, _currentRequest.PrimaryId, context.HttpContext.Request.GetDisplayUrl()); + throw new ResourceIdMismatchException(bodyId, _request.PrimaryId, context.HttpContext.Request.GetDisplayUrl()); } } - - return await InputFormatterResult.SuccessAsync(model); } /// Checks if the deserialized payload has an ID included diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs similarity index 81% rename from src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs rename to src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs index e9f1c95f6f..bc0c5b44b1 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs @@ -4,34 +4,34 @@ using System.Net.Http; using System.Text; using System.Threading.Tasks; -using JsonApiDotNetCore.Exceptions; +using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.Models.JsonApiDocuments; -using JsonApiDotNetCore.Serialization.Server; +using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -namespace JsonApiDotNetCore.Formatters +namespace JsonApiDotNetCore.Serialization { /// - /// Formats the response data used https://docs.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting?view=aspnetcore-3.0. + /// Formats the response data used (see https://docs.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting?view=aspnetcore-3.0). /// It was intended to have as little dependencies as possible in formatting layer for greater extensibility. /// public class JsonApiWriter : IJsonApiWriter { private readonly IJsonApiSerializer _serializer; private readonly IExceptionHandler _exceptionHandler; - private readonly ILogger _logger; + private readonly TraceLogWriter _traceWriter; public JsonApiWriter(IJsonApiSerializer serializer, IExceptionHandler exceptionHandler, ILoggerFactory loggerFactory) { - _serializer = serializer; - _exceptionHandler = exceptionHandler; + if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); - _logger = loggerFactory.CreateLogger(); + _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); + _exceptionHandler = exceptionHandler ?? throw new ArgumentNullException(nameof(exceptionHandler)); + _traceWriter = new TraceLogWriter(loggerFactory); } public async Task WriteAsync(OutputFormatterWriteContext context) @@ -64,7 +64,7 @@ public async Task WriteAsync(OutputFormatterWriteContext context) } var url = context.HttpContext.Request.GetEncodedUrl(); - _logger.LogTrace($"Sending {response.StatusCode} response for request at '{url}' with body: <<{responseContent}>>"); + _traceWriter.LogMessage(() => $"Sending {response.StatusCode} response for request at '{url}' with body: <<{responseContent}>>"); await writer.WriteAsync(responseContent); await writer.FlushAsync(); diff --git a/src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs b/src/JsonApiDotNetCore/Serialization/JsonSerializerExtensions.cs similarity index 98% rename from src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs rename to src/JsonApiDotNetCore/Serialization/JsonSerializerExtensions.cs index 38dde88f63..e08b9c3ce0 100644 --- a/src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonSerializerExtensions.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -namespace JsonApiDotNetCore.Extensions +namespace JsonApiDotNetCore.Serialization { internal static class JsonSerializerExtensions { diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/Document.cs b/src/JsonApiDotNetCore/Serialization/Objects/Document.cs similarity index 79% rename from src/JsonApiDotNetCore/Models/JsonApiDocuments/Document.cs rename to src/JsonApiDotNetCore/Serialization/Objects/Document.cs index ef96baea70..0be16785de 100644 --- a/src/JsonApiDotNetCore/Models/JsonApiDocuments/Document.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/Document.cs @@ -1,30 +1,29 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Models.JsonApiDocuments; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Models -{ - /// - /// https://jsonapi.org/format/#document-structure - /// - public sealed class Document : ExposableData - { - /// - /// see "meta" in https://jsonapi.org/format/#document-top-level - /// - [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] - public Dictionary Meta { get; set; } - - /// - /// see "links" in https://jsonapi.org/format/#document-top-level - /// - [JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)] - public TopLevelLinks Links { get; set; } - - /// - /// see "included" in https://jsonapi.org/format/#document-top-level - /// - [JsonProperty("included", NullValueHandling = NullValueHandling.Ignore, Order = 1)] - public List Included { get; set; } - } -} +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.Serialization.Objects +{ + /// + /// https://jsonapi.org/format/#document-structure + /// + public sealed class Document : ExposableData + { + /// + /// see "meta" in https://jsonapi.org/format/#document-top-level + /// + [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] + public IDictionary Meta { get; set; } + + /// + /// see "links" in https://jsonapi.org/format/#document-top-level + /// + [JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)] + public TopLevelLinks Links { get; set; } + + /// + /// see "included" in https://jsonapi.org/format/#document-top-level + /// + [JsonProperty("included", NullValueHandling = NullValueHandling.Ignore, Order = 1)] + public IList Included { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/Error.cs b/src/JsonApiDotNetCore/Serialization/Objects/Error.cs similarity index 96% rename from src/JsonApiDotNetCore/Models/JsonApiDocuments/Error.cs rename to src/JsonApiDotNetCore/Serialization/Objects/Error.cs index b93c3c0790..32b95428d1 100644 --- a/src/JsonApiDotNetCore/Models/JsonApiDocuments/Error.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/Error.cs @@ -3,11 +3,11 @@ using System.Net; using Newtonsoft.Json; -namespace JsonApiDotNetCore.Models.JsonApiDocuments +namespace JsonApiDotNetCore.Serialization.Objects { /// /// Provides additional information about a problem encountered while performing an operation. - /// Error objects MUST be returned as an array keyed by errors in the top level of a JSON:API document. + /// Error objects MUST be returned as an array keyed by errors in the top level of a json:api document. /// public sealed class Error { diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ErrorDocument.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorDocument.cs similarity index 72% rename from src/JsonApiDotNetCore/Models/JsonApiDocuments/ErrorDocument.cs rename to src/JsonApiDotNetCore/Serialization/Objects/ErrorDocument.cs index 452b12adbc..34b89596e4 100644 --- a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ErrorDocument.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ErrorDocument.cs @@ -1,25 +1,28 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Net; -namespace JsonApiDotNetCore.Models.JsonApiDocuments +namespace JsonApiDotNetCore.Serialization.Objects { public sealed class ErrorDocument { public IReadOnlyList Errors { get; } public ErrorDocument() + : this(Array.Empty()) { - Errors = new List(); } public ErrorDocument(Error error) + : this(new[] {error}) { - Errors = new List {error}; } public ErrorDocument(IEnumerable errors) { + if (errors == null) throw new ArgumentNullException(nameof(errors)); + Errors = errors.ToList(); } @@ -28,9 +31,9 @@ public HttpStatusCode GetErrorStatusCode() var statusCodes = Errors .Select(e => (int)e.StatusCode) .Distinct() - .ToList(); + .ToArray(); - if (statusCodes.Count == 1) + if (statusCodes.Length == 1) return (HttpStatusCode)statusCodes[0]; var statusCode = int.Parse(statusCodes.Max().ToString()[0] + "00"); diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ErrorLinks.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorLinks.cs similarity index 84% rename from src/JsonApiDotNetCore/Models/JsonApiDocuments/ErrorLinks.cs rename to src/JsonApiDotNetCore/Serialization/Objects/ErrorLinks.cs index a2c06ff1af..16be6392e1 100644 --- a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ErrorLinks.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ErrorLinks.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace JsonApiDotNetCore.Models.JsonApiDocuments +namespace JsonApiDotNetCore.Serialization.Objects { public sealed class ErrorLinks { diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ErrorMeta.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorMeta.cs similarity index 71% rename from src/JsonApiDotNetCore/Models/JsonApiDocuments/ErrorMeta.cs rename to src/JsonApiDotNetCore/Serialization/Objects/ErrorMeta.cs index 6d09bd627c..8a07774d71 100644 --- a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ErrorMeta.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ErrorMeta.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using Newtonsoft.Json; -namespace JsonApiDotNetCore.Models.JsonApiDocuments +namespace JsonApiDotNetCore.Serialization.Objects { /// /// A meta object containing non-standard meta-information about the error. @@ -11,7 +11,7 @@ namespace JsonApiDotNetCore.Models.JsonApiDocuments public sealed class ErrorMeta { [JsonExtensionData] - public Dictionary Data { get; } = new Dictionary(); + public IDictionary Data { get; } = new Dictionary(); public void IncludeExceptionStackTrace(Exception exception) { @@ -22,7 +22,7 @@ public void IncludeExceptionStackTrace(Exception exception) else { Data["StackTrace"] = exception.Demystify().ToString() - .Split(new[] { "\n" }, int.MaxValue, StringSplitOptions.RemoveEmptyEntries); + .Split("\n", int.MaxValue, StringSplitOptions.RemoveEmptyEntries); } } } diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ErrorSource.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorSource.cs similarity index 91% rename from src/JsonApiDotNetCore/Models/JsonApiDocuments/ErrorSource.cs rename to src/JsonApiDotNetCore/Serialization/Objects/ErrorSource.cs index 7b39aa564e..0e1686fbdf 100644 --- a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ErrorSource.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ErrorSource.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace JsonApiDotNetCore.Models.JsonApiDocuments +namespace JsonApiDotNetCore.Serialization.Objects { public sealed class ErrorSource { diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ExposableData.cs b/src/JsonApiDotNetCore/Serialization/Objects/ExposableData.cs similarity index 72% rename from src/JsonApiDotNetCore/Models/JsonApiDocuments/ExposableData.cs rename to src/JsonApiDotNetCore/Serialization/Objects/ExposableData.cs index cf09483775..aa2b603406 100644 --- a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ExposableData.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ExposableData.cs @@ -1,14 +1,14 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace JsonApiDotNetCore.Models +namespace JsonApiDotNetCore.Serialization.Objects { - public abstract class ExposableData where T : class + public abstract class ExposableData where TResource : class { /// - /// see "primary data" in https://jsonapi.org/format/#document-top-level. + /// See "primary data" in https://jsonapi.org/format/#document-top-level. /// [JsonProperty("data")] public object Data @@ -18,7 +18,7 @@ public object Data } /// - /// see https://www.newtonsoft.com/json/help/html/ConditionalProperties.htm + /// See https://www.newtonsoft.com/json/help/html/ConditionalProperties.htm. /// /// /// Moving this method to the derived class where it is needed only in the @@ -36,16 +36,16 @@ public bool ShouldSerializeData() /// Internally used for "single" primary data. /// [JsonIgnore] - public T SingleData { get; private set; } + public TResource SingleData { get; private set; } /// /// Internally used for "many" primary data. /// [JsonIgnore] - public List ManyData { get; private set; } + public IList ManyData { get; private set; } /// - /// Used to indicate if the document's primary data is "single" or "many". + /// Indicates if the document's primary data is "single" or "many". /// [JsonIgnore] public bool IsManyData { get; private set; } @@ -57,7 +57,11 @@ public bool ShouldSerializeData() /// internal bool IsPopulated { get; private set; } - internal bool HasResource => IsPopulated && ((IsManyData && ManyData.Any()) || SingleData != null); + internal bool HasResource => IsPopulated && !IsEmpty; + + private bool IsEmpty => !HasManyData && SingleData == null; + + private bool HasManyData => IsManyData && ManyData.Any(); /// /// Gets the "single" or "many" data depending on which one was @@ -77,16 +81,16 @@ protected void SetPrimaryData(object value) { IsPopulated = true; if (value is JObject jObject) - SingleData = jObject.ToObject(); - else if (value is T ro) + SingleData = jObject.ToObject(); + else if (value is TResource ro) SingleData = ro; else if (value != null) { IsManyData = true; if (value is JArray jArray) - ManyData = jArray.ToObject>(); + ManyData = jArray.ToObject>(); else - ManyData = (List)value; + ManyData = (List)value; } } } diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/RelationshipEntry.cs b/src/JsonApiDotNetCore/Serialization/Objects/RelationshipEntry.cs similarity index 75% rename from src/JsonApiDotNetCore/Models/JsonApiDocuments/RelationshipEntry.cs rename to src/JsonApiDotNetCore/Serialization/Objects/RelationshipEntry.cs index 9a9359767c..6228f83972 100644 --- a/src/JsonApiDotNetCore/Models/JsonApiDocuments/RelationshipEntry.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/RelationshipEntry.cs @@ -1,7 +1,6 @@ -using JsonApiDotNetCore.Models.JsonApiDocuments; using Newtonsoft.Json; -namespace JsonApiDotNetCore.Models +namespace JsonApiDotNetCore.Serialization.Objects { public sealed class RelationshipEntry : ExposableData { diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/RelationshipLinks.cs b/src/JsonApiDotNetCore/Serialization/Objects/RelationshipLinks.cs similarity index 62% rename from src/JsonApiDotNetCore/Models/JsonApiDocuments/RelationshipLinks.cs rename to src/JsonApiDotNetCore/Serialization/Objects/RelationshipLinks.cs index 1ac1d67c7d..fd747eea8e 100644 --- a/src/JsonApiDotNetCore/Models/JsonApiDocuments/RelationshipLinks.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/RelationshipLinks.cs @@ -1,17 +1,17 @@ using Newtonsoft.Json; -namespace JsonApiDotNetCore.Models.JsonApiDocuments +namespace JsonApiDotNetCore.Serialization.Objects { public sealed class RelationshipLinks { /// - /// see "links" bulletin at https://jsonapi.org/format/#document-resource-object-relationships + /// See "links" bulletin at https://jsonapi.org/format/#document-resource-object-relationships. /// [JsonProperty("self", NullValueHandling = NullValueHandling.Ignore)] public string Self { get; set; } /// - /// https://jsonapi.org/format/#document-resource-object-related-resource-links + /// See https://jsonapi.org/format/#document-resource-object-related-resource-links. /// [JsonProperty("related", NullValueHandling = NullValueHandling.Ignore)] public string Related { get; set; } diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceIdentifierObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs similarity index 91% rename from src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceIdentifierObject.cs rename to src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs index 939cae0820..af10bc0cab 100644 --- a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceIdentifierObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs @@ -1,10 +1,11 @@ using Newtonsoft.Json; -namespace JsonApiDotNetCore.Models +namespace JsonApiDotNetCore.Serialization.Objects { public class ResourceIdentifierObject { public ResourceIdentifierObject() { } + public ResourceIdentifierObject(string type, string id) { Type = type; diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceLinks.cs b/src/JsonApiDotNetCore/Serialization/Objects/ResourceLinks.cs similarity index 65% rename from src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceLinks.cs rename to src/JsonApiDotNetCore/Serialization/Objects/ResourceLinks.cs index 4997d42f27..701e42a3f1 100644 --- a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceLinks.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ResourceLinks.cs @@ -1,11 +1,11 @@ using Newtonsoft.Json; -namespace JsonApiDotNetCore.Models.JsonApiDocuments +namespace JsonApiDotNetCore.Serialization.Objects { public sealed class ResourceLinks { /// - /// https://jsonapi.org/format/#document-resource-object-links + /// See https://jsonapi.org/format/#document-resource-object-links. /// [JsonProperty("self", NullValueHandling = NullValueHandling.Ignore)] public string Self { get; set; } diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs similarity index 65% rename from src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceObject.cs rename to src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs index a9d619191e..787bfe292e 100644 --- a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ResourceObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs @@ -1,16 +1,15 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Models.JsonApiDocuments; using Newtonsoft.Json; -namespace JsonApiDotNetCore.Models +namespace JsonApiDotNetCore.Serialization.Objects { public sealed class ResourceObject : ResourceIdentifierObject { [JsonProperty("attributes", NullValueHandling = NullValueHandling.Ignore)] - public Dictionary Attributes { get; set; } + public IDictionary Attributes { get; set; } [JsonProperty("relationships", NullValueHandling = NullValueHandling.Ignore)] - public Dictionary Relationships { get; set; } + public IDictionary Relationships { get; set; } [JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)] public ResourceLinks Links { get; set; } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/TopLevelLinks.cs b/src/JsonApiDotNetCore/Serialization/Objects/TopLevelLinks.cs new file mode 100644 index 0000000000..96adaf99e9 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Objects/TopLevelLinks.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.Serialization.Objects +{ + /// + /// See links section in https://jsonapi.org/format/#document-top-level. + /// + public sealed class TopLevelLinks + { + [JsonProperty("self")] + public string Self { get; set; } + + [JsonProperty("first")] + public string First { get; set; } + + [JsonProperty("last")] + public string Last { get; set; } + + [JsonProperty("prev")] + public string Prev { get; set; } + + [JsonProperty("next")] + public string Next { get; set; } + + // http://www.newtonsoft.com/json/help/html/ConditionalProperties.htm + public bool ShouldSerializeSelf() => !string.IsNullOrEmpty(Self); + public bool ShouldSerializeFirst() => !string.IsNullOrEmpty(First); + public bool ShouldSerializeLast() => !string.IsNullOrEmpty(Last); + public bool ShouldSerializePrev() => !string.IsNullOrEmpty(Prev); + public bool ShouldSerializeNext() => !string.IsNullOrEmpty(Next); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs b/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs similarity index 65% rename from src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs rename to src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs index 817e2bf919..e7b3734b2f 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs @@ -1,44 +1,47 @@ -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; -using Microsoft.AspNetCore.Http; +using System; using System.Collections.Generic; -using System.Reflection; -using JsonApiDotNetCore.Extensions; using System.Net.Http; +using System.Reflection; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.AspNetCore.Http; -namespace JsonApiDotNetCore.Serialization.Server +namespace JsonApiDotNetCore.Serialization { /// - /// Server deserializer implementation of the + /// Server deserializer implementation of the . /// - public class RequestDeserializer : BaseDocumentParser, IJsonApiDeserializer + public class RequestDeserializer : BaseDeserializer, IJsonApiDeserializer { private readonly ITargetedFields _targetedFields; private readonly IHttpContextAccessor _httpContextAccessor; - public RequestDeserializer(IResourceContextProvider contextProvider, IResourceFactory resourceFactory, ITargetedFields targetedFields, IHttpContextAccessor httpContextAccessor) - : base(contextProvider, resourceFactory) + public RequestDeserializer(IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory, ITargetedFields targetedFields, IHttpContextAccessor httpContextAccessor) + : base(resourceContextProvider, resourceFactory) { - _targetedFields = targetedFields; - _httpContextAccessor = httpContextAccessor; + _targetedFields = targetedFields ?? throw new ArgumentNullException(nameof(targetedFields)); + _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } - /// - public new object Deserialize(string body) + /// + public object Deserialize(string body) { - return base.Deserialize(body); + if (body == null) throw new ArgumentNullException(nameof(body)); + + return DeserializeBody(body); } /// /// Additional processing required for server deserialization. Flags a /// processed attribute or relationship as updated using . /// - /// The resource that was constructed from the document's body - /// The metadata for the exposed field - /// Relationship data for . Is null when is not a + /// The resource that was constructed from the document's body. + /// The metadata for the exposed field. + /// Relationship data for . Is null when is not a . protected override void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipEntry data = null) { if (field is AttrAttribute attr) @@ -58,8 +61,11 @@ protected override void AfterProcessField(IIdentifiable resource, ResourceFieldA _targetedFields.Relationships.Add(relationship); } - protected override IIdentifiable SetAttributes(IIdentifiable resource, Dictionary attributeValues, List attributes) + protected override IIdentifiable SetAttributes(IIdentifiable resource, IDictionary attributeValues, IReadOnlyCollection attributes) { + if (resource == null) throw new ArgumentNullException(nameof(resource)); + if (attributes == null) throw new ArgumentNullException(nameof(attributes)); + if (_httpContextAccessor.HttpContext.Request.Method == HttpMethod.Patch.Method) { foreach (AttrAttribute attr in attributes) @@ -80,8 +86,11 @@ protected override IIdentifiable SetAttributes(IIdentifiable resource, Dictionar return base.SetAttributes(resource, attributeValues, attributes); } - protected override IIdentifiable SetRelationships(IIdentifiable resource, Dictionary relationshipsValues, List relationshipAttributes) + protected override IIdentifiable SetRelationships(IIdentifiable resource, IDictionary relationshipsValues, IReadOnlyCollection relationshipAttributes) { + if (resource == null) throw new ArgumentNullException(nameof(resource)); + if (relationshipAttributes == null) throw new ArgumentNullException(nameof(relationshipAttributes)); + // If there is a relationship included in the data of the POST or PATCH, then the 'IsRequired' attribute will be disabled for any // property within that object. For instance, a new article is posted and has a relationship included to an author. In this case, // the author name (which has the 'IsRequired' attribute) will not be included in the POST. Unless disabled, the POST will fail. diff --git a/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs similarity index 75% rename from src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs rename to src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs index 1c172018c4..5639ed5a77 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs @@ -1,31 +1,32 @@ using System; using System.Collections.Generic; +using System.Linq; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Building; +using JsonApiDotNetCore.Serialization.Objects; using Newtonsoft.Json; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Serialization.Server.Builders; -using JsonApiDotNetCore.Models.JsonApiDocuments; -using JsonApiDotNetCore.RequestServices.Contracts; -namespace JsonApiDotNetCore.Serialization.Server +namespace JsonApiDotNetCore.Serialization { /// - /// Server serializer implementation of + /// Server serializer implementation of /// /// /// Because in JsonApiDotNetCore every json:api request is associated with exactly one - /// resource (the primary resource, see ), + /// resource (the primary resource, see ), /// the serializer can leverage this information using generics. /// See for how this is instantiated. /// /// Type of the resource associated with the scope of the request /// for which this serializer is used. - public class ResponseSerializer : BaseDocumentBuilder, IJsonApiSerializer, IResponseSerializer + public class ResponseSerializer : BaseSerializer, IJsonApiSerializer, IResponseSerializer where TResource : class, IIdentifiable { public RelationshipAttribute RequestRelationship { get; set; } + private readonly IFieldsToSerialize _fieldsToSerialize; private readonly IJsonApiOptions _options; private readonly IMetaBuilder _metaBuilder; @@ -41,15 +42,15 @@ public ResponseSerializer(IMetaBuilder metaBuilder, IJsonApiOptions options) : base(resourceObjectBuilder) { - _fieldsToSerialize = fieldsToSerialize; - _options = options; - _linkBuilder = linkBuilder; - _metaBuilder = metaBuilder; - _includedBuilder = includedBuilder; + _fieldsToSerialize = fieldsToSerialize ?? throw new ArgumentNullException(nameof(fieldsToSerialize)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _linkBuilder = linkBuilder ?? throw new ArgumentNullException(nameof(linkBuilder)); + _metaBuilder = metaBuilder ?? throw new ArgumentNullException(nameof(metaBuilder)); + _includedBuilder = includedBuilder ?? throw new ArgumentNullException(nameof(includedBuilder)); _primaryResourceType = typeof(TResource); } - /// + /// public string Serialize(object data) { if (data == null || data is IIdentifiable) @@ -59,7 +60,7 @@ public string Serialize(object data) if (data is IEnumerable collectionOfIdentifiable) { - return SerializeMany(collectionOfIdentifiable); + return SerializeMany(collectionOfIdentifiable.ToArray()); } if (data is ErrorDocument errorDocument) @@ -76,16 +77,16 @@ private string SerializeErrorDocument(ErrorDocument errorDocument) } /// - /// Convert a single resource into a serialized + /// Converts a single resource into a serialized . /// /// - /// This method is set internal instead of private for easier testability. + /// This method is internal instead of private for easier testability. /// internal string SerializeSingle(IIdentifiable resource) { if (RequestRelationship != null && resource != null) { - var relationship = ((ResponseResourceObjectBuilder)_resourceObjectBuilder).Build(resource, RequestRelationship); + var relationship = ((ResponseResourceObjectBuilder)ResourceObjectBuilder).Build(resource, RequestRelationship); return SerializeObject(relationship, _options.SerializerSettings, serializer => { serializer.NullValueHandling = NullValueHandling.Include; }); } @@ -106,12 +107,12 @@ internal string SerializeSingle(IIdentifiable resource) } /// - /// Convert a list of resources into a serialized + /// Converts a collection of resources into a serialized . /// /// - /// This method is set internal instead of private for easier testability. + /// This method is internal instead of private for easier testability. /// - internal string SerializeMany(IEnumerable resources) + internal string SerializeMany(IReadOnlyCollection resources) { var (attributes, relationships) = GetFieldsToSerialize(); var document = Build(resources, attributes, relationships); diff --git a/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializerFactory.cs b/src/JsonApiDotNetCore/Serialization/ResponseSerializerFactory.cs similarity index 55% rename from src/JsonApiDotNetCore/Serialization/Server/ResponseSerializerFactory.cs rename to src/JsonApiDotNetCore/Serialization/ResponseSerializerFactory.cs index 3b3947d6e6..356c606833 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializerFactory.cs +++ b/src/JsonApiDotNetCore/Serialization/ResponseSerializerFactory.cs @@ -1,23 +1,22 @@ using System; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.RequestServices.Contracts; -using JsonApiDotNetCore.Services; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; -namespace JsonApiDotNetCore.Serialization.Server +namespace JsonApiDotNetCore.Serialization { /// /// A factory class to abstract away the initialization of the serializer from the - /// .net core formatter pipeline. + /// ASP.NET Core formatter pipeline. /// public class ResponseSerializerFactory : IJsonApiSerializerFactory { private readonly IServiceProvider _provider; - private readonly ICurrentRequest _currentRequest; + private readonly IJsonApiRequest _request; - public ResponseSerializerFactory(ICurrentRequest currentRequest, IScopedServiceProvider provider) + public ResponseSerializerFactory(IJsonApiRequest request, IRequestScopedServiceProvider provider) { - _currentRequest = currentRequest; - _provider = provider; + _request = request ?? throw new ArgumentNullException(nameof(request)); + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); } /// @@ -30,15 +29,15 @@ public IJsonApiSerializer GetSerializer() var serializerType = typeof(ResponseSerializer<>).MakeGenericType(targetType); var serializer = (IResponseSerializer)_provider.GetService(serializerType); - if (_currentRequest.Kind == EndpointKind.Relationship && _currentRequest.Relationship != null) - serializer.RequestRelationship = _currentRequest.Relationship; + if (_request.Kind == EndpointKind.Relationship && _request.Relationship != null) + serializer.RequestRelationship = _request.Relationship; return (IJsonApiSerializer)serializer; } private Type GetDocumentType() { - var resourceContext = _currentRequest.SecondaryResource ?? _currentRequest.PrimaryResource; + var resourceContext = _request.SecondaryResource ?? _request.PrimaryResource; return resourceContext.ResourceType; } } diff --git a/src/JsonApiDotNetCore/Services/Contract/ICreateService.cs b/src/JsonApiDotNetCore/Services/Contract/ICreateService.cs deleted file mode 100644 index 832e893dd0..0000000000 --- a/src/JsonApiDotNetCore/Services/Contract/ICreateService.cs +++ /dev/null @@ -1,15 +0,0 @@ -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 resource); - } -} diff --git a/src/JsonApiDotNetCore/Services/Contract/IDeleteService.cs b/src/JsonApiDotNetCore/Services/Contract/IDeleteService.cs deleted file mode 100644 index 8ee8c11b12..0000000000 --- a/src/JsonApiDotNetCore/Services/Contract/IDeleteService.cs +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 75eb9c6799..0000000000 --- a/src/JsonApiDotNetCore/Services/Contract/IGetAllService.cs +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index c01c6a1391..0000000000 --- a/src/JsonApiDotNetCore/Services/Contract/IGetByIdService.cs +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index f4eec1eddc..0000000000 --- a/src/JsonApiDotNetCore/Services/Contract/IGetRelationshipService.cs +++ /dev/null @@ -1,15 +0,0 @@ -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/IGetSecondaryService.cs b/src/JsonApiDotNetCore/Services/Contract/IGetSecondaryService.cs deleted file mode 100644 index 6f61eaca0c..0000000000 --- a/src/JsonApiDotNetCore/Services/Contract/IGetSecondaryService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Threading.Tasks; -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCore.Services -{ - public interface IGetSecondaryService : IGetSecondaryService - where T : class, IIdentifiable - { } - - public interface IGetSecondaryService - where T : class, IIdentifiable - { - Task GetSecondaryAsync(TId id, string relationshipName); - } -} diff --git a/src/JsonApiDotNetCore/Services/Contract/IResourceCommandService.cs b/src/JsonApiDotNetCore/Services/Contract/IResourceCommandService.cs deleted file mode 100644 index 252090b2d1..0000000000 --- a/src/JsonApiDotNetCore/Services/Contract/IResourceCommandService.cs +++ /dev/null @@ -1,21 +0,0 @@ -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCore.Services -{ - public interface IResourceCommandService : - ICreateService, - IUpdateService, - IUpdateRelationshipService, - IDeleteService, - IResourceCommandService - where T : class, IIdentifiable - { } - - public interface IResourceCommandService : - ICreateService, - IUpdateService, - IUpdateRelationshipService, - IDeleteService - where T : class, IIdentifiable - { } -} diff --git a/src/JsonApiDotNetCore/Services/Contract/IResourceQueryService.cs b/src/JsonApiDotNetCore/Services/Contract/IResourceQueryService.cs deleted file mode 100644 index 5cc8c03d3a..0000000000 --- a/src/JsonApiDotNetCore/Services/Contract/IResourceQueryService.cs +++ /dev/null @@ -1,21 +0,0 @@ -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCore.Services -{ - public interface IResourceQueryService : - IGetAllService, - IGetByIdService, - IGetRelationshipService, - IGetSecondaryService, - IResourceQueryService - where T : class, IIdentifiable - { } - - public interface IResourceQueryService : - IGetAllService, - IGetByIdService, - IGetRelationshipService, - IGetSecondaryService - where T : class, IIdentifiable - { } -} diff --git a/src/JsonApiDotNetCore/Services/Contract/IResourceService.cs b/src/JsonApiDotNetCore/Services/Contract/IResourceService.cs deleted file mode 100644 index 0f221c4509..0000000000 --- a/src/JsonApiDotNetCore/Services/Contract/IResourceService.cs +++ /dev/null @@ -1,14 +0,0 @@ -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCore.Services -{ - public interface IResourceService - : IResourceCommandService, IResourceQueryService, IResourceService - where T : class, IIdentifiable - { } - - public interface IResourceService - : IResourceCommandService, IResourceQueryService - where T : class, IIdentifiable - { } -} diff --git a/src/JsonApiDotNetCore/Services/Contract/IUpdateRelationshipService.cs b/src/JsonApiDotNetCore/Services/Contract/IUpdateRelationshipService.cs deleted file mode 100644 index 85b6acab3d..0000000000 --- a/src/JsonApiDotNetCore/Services/Contract/IUpdateRelationshipService.cs +++ /dev/null @@ -1,15 +0,0 @@ -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 UpdateRelationshipAsync(TId id, string relationshipName, object relationships); - } -} diff --git a/src/JsonApiDotNetCore/Services/Contract/IUpdateService.cs b/src/JsonApiDotNetCore/Services/Contract/IUpdateService.cs deleted file mode 100644 index 4f9de8744e..0000000000 --- a/src/JsonApiDotNetCore/Services/Contract/IUpdateService.cs +++ /dev/null @@ -1,15 +0,0 @@ -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 resource); - } -} diff --git a/src/JsonApiDotNetCore/Services/ICreateService.cs b/src/JsonApiDotNetCore/Services/ICreateService.cs new file mode 100644 index 0000000000..d49a9324e4 --- /dev/null +++ b/src/JsonApiDotNetCore/Services/ICreateService.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Services +{ + /// + public interface ICreateService : ICreateService + where TResource : class, IIdentifiable + { } + + /// + public interface ICreateService + where TResource : class, IIdentifiable + { + /// + /// Handles a json:api request to create a new resource. + /// + Task CreateAsync(TResource resource); + } +} diff --git a/src/JsonApiDotNetCore/Services/IDeleteService.cs b/src/JsonApiDotNetCore/Services/IDeleteService.cs new file mode 100644 index 0000000000..92b5979b14 --- /dev/null +++ b/src/JsonApiDotNetCore/Services/IDeleteService.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Services +{ + /// + public interface IDeleteService : IDeleteService + where TResource : class, IIdentifiable + { } + + /// + public interface IDeleteService + where TResource : class, IIdentifiable + { + /// + /// Handles a json:api request to delete an existing resource. + /// + Task DeleteAsync(TId id); + } +} diff --git a/src/JsonApiDotNetCore/Services/IGetAllService.cs b/src/JsonApiDotNetCore/Services/IGetAllService.cs new file mode 100644 index 0000000000..e13ad53dce --- /dev/null +++ b/src/JsonApiDotNetCore/Services/IGetAllService.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Services +{ + /// + public interface IGetAllService : IGetAllService + where TResource : class, IIdentifiable + { } + + /// + public interface IGetAllService + where TResource : class, IIdentifiable + { + /// + /// Handles a json:api request to retrieve a collection of resources for a primary endpoint. + /// + Task> GetAsync(); + } +} diff --git a/src/JsonApiDotNetCore/Services/IGetByIdService.cs b/src/JsonApiDotNetCore/Services/IGetByIdService.cs new file mode 100644 index 0000000000..768b5a6d79 --- /dev/null +++ b/src/JsonApiDotNetCore/Services/IGetByIdService.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Services +{ + /// + public interface IGetByIdService : IGetByIdService + where TResource : class, IIdentifiable + { } + + /// + public interface IGetByIdService + where TResource : class, IIdentifiable + { + /// + /// Handles a json:api request to retrieve a single resource for a primary endpoint. + /// + Task GetAsync(TId id); + } +} diff --git a/src/JsonApiDotNetCore/Services/IGetRelationshipService.cs b/src/JsonApiDotNetCore/Services/IGetRelationshipService.cs new file mode 100644 index 0000000000..7cd926b3a0 --- /dev/null +++ b/src/JsonApiDotNetCore/Services/IGetRelationshipService.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Services +{ + /// + public interface IGetRelationshipService : IGetRelationshipService + where TResource : class, IIdentifiable + { } + + /// + public interface IGetRelationshipService + where TResource : class, IIdentifiable + { + /// + /// Handles a json:api request to retrieve a single relationship. + /// + Task GetRelationshipAsync(TId id, string relationshipName); + } +} diff --git a/src/JsonApiDotNetCore/Services/IGetSecondaryService.cs b/src/JsonApiDotNetCore/Services/IGetSecondaryService.cs new file mode 100644 index 0000000000..dbe52cf6f9 --- /dev/null +++ b/src/JsonApiDotNetCore/Services/IGetSecondaryService.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Services +{ + /// + public interface IGetSecondaryService : IGetSecondaryService + where TResource : class, IIdentifiable + { } + + /// + public interface IGetSecondaryService + where TResource : class, IIdentifiable + { + /// + /// Handles a json:api request to retrieve a single resource or a collection of resources for a secondary endpoint, such as /articles/1/author or /articles/1/revisions. + /// + Task GetSecondaryAsync(TId id, string relationshipName); + } +} diff --git a/src/JsonApiDotNetCore/Services/IResourceCommandService.cs b/src/JsonApiDotNetCore/Services/IResourceCommandService.cs new file mode 100644 index 0000000000..c756d3a87b --- /dev/null +++ b/src/JsonApiDotNetCore/Services/IResourceCommandService.cs @@ -0,0 +1,30 @@ +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Services +{ + /// + /// Groups write operations. + /// + /// The resource type. + public interface IResourceCommandService : + ICreateService, + IUpdateService, + IUpdateRelationshipService, + IDeleteService, + IResourceCommandService + where TResource : class, IIdentifiable + { } + + /// + /// Groups write operations. + /// + /// The resource type. + /// The resource identifier type. + public interface IResourceCommandService : + ICreateService, + IUpdateService, + IUpdateRelationshipService, + IDeleteService + where TResource : class, IIdentifiable + { } +} diff --git a/src/JsonApiDotNetCore/Services/IResourceQueryService.cs b/src/JsonApiDotNetCore/Services/IResourceQueryService.cs new file mode 100644 index 0000000000..dd337a6bfc --- /dev/null +++ b/src/JsonApiDotNetCore/Services/IResourceQueryService.cs @@ -0,0 +1,30 @@ +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Services +{ + /// + /// Groups read operations. + /// + /// The resource type. + public interface IResourceQueryService : + IGetAllService, + IGetByIdService, + IGetRelationshipService, + IGetSecondaryService, + IResourceQueryService + where TResource : class, IIdentifiable + { } + + /// + /// Groups read operations. + /// + /// The resource type. + /// The resource identifier type. + public interface IResourceQueryService : + IGetAllService, + IGetByIdService, + IGetRelationshipService, + IGetSecondaryService + where TResource : class, IIdentifiable + { } +} diff --git a/src/JsonApiDotNetCore/Services/IResourceService.cs b/src/JsonApiDotNetCore/Services/IResourceService.cs new file mode 100644 index 0000000000..126f6b43b1 --- /dev/null +++ b/src/JsonApiDotNetCore/Services/IResourceService.cs @@ -0,0 +1,23 @@ +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Services +{ + /// + /// Represents the foundational Resource Service layer in the JsonApiDotNetCore architecture that uses a Resource Repository for data access. + /// + /// The resource type. + public interface IResourceService + : IResourceCommandService, IResourceQueryService, IResourceService + where TResource : class, IIdentifiable + { } + + /// + /// Represents the foundational Resource Service layer in the JsonApiDotNetCore architecture that uses a Resource Repository for data access. + /// + /// The resource type. + /// The resource identifier type. + public interface IResourceService + : IResourceCommandService, IResourceQueryService + where TResource : class, IIdentifiable + { } +} diff --git a/src/JsonApiDotNetCore/Services/IUpdateRelationshipService.cs b/src/JsonApiDotNetCore/Services/IUpdateRelationshipService.cs new file mode 100644 index 0000000000..0b3b27fa9f --- /dev/null +++ b/src/JsonApiDotNetCore/Services/IUpdateRelationshipService.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Services +{ + /// + public interface IUpdateRelationshipService : IUpdateRelationshipService + where TResource : class, IIdentifiable + { } + + /// + public interface IUpdateRelationshipService + where TResource : class, IIdentifiable + { + /// + /// Handles a json:api request to update an existing relationship. + /// + Task UpdateRelationshipAsync(TId id, string relationshipName, object relationships); + } +} diff --git a/src/JsonApiDotNetCore/Services/IUpdateService.cs b/src/JsonApiDotNetCore/Services/IUpdateService.cs new file mode 100644 index 0000000000..c34b8ed511 --- /dev/null +++ b/src/JsonApiDotNetCore/Services/IUpdateService.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Services +{ + /// + public interface IUpdateService : IUpdateService + where TResource : class, IIdentifiable + { } + + /// + public interface IUpdateService + where TResource : class, IIdentifiable + { + /// + /// Handles a json:api request to update an existing resource. + /// + Task UpdateAsync(TId id, TResource resource); + } +} diff --git a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs index 106d14a770..2a101f22c9 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs @@ -1,22 +1,22 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Hooks; -using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Hooks.Internal; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.RequestServices; -using JsonApiDotNetCore.RequestServices.Contracts; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Services { + /// public class JsonApiResourceService : IResourceService where TResource : class, IIdentifiable @@ -25,8 +25,8 @@ public class JsonApiResourceService : private readonly IQueryLayerComposer _queryLayerComposer; private readonly IPaginationContext _paginationContext; private readonly IJsonApiOptions _options; - private readonly ICurrentRequest _currentRequest; - private readonly ILogger _logger; + private readonly TraceLogWriter> _traceWriter; + private readonly IJsonApiRequest _request; private readonly IResourceChangeTracker _resourceChangeTracker; private readonly IResourceFactory _resourceFactory; private readonly IResourceHookExecutor _hookExecutor; @@ -37,25 +37,29 @@ public JsonApiResourceService( IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, - ICurrentRequest currentRequest, + IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker, IResourceFactory resourceFactory, IResourceHookExecutor hookExecutor = null) { - _repository = repository; - _queryLayerComposer = queryLayerComposer; - _paginationContext = paginationContext; - _options = options; - _currentRequest = currentRequest; - _logger = loggerFactory.CreateLogger>(); - _resourceChangeTracker = resourceChangeTracker; - _resourceFactory = resourceFactory; + if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); + + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + _queryLayerComposer = queryLayerComposer ?? throw new ArgumentNullException(nameof(queryLayerComposer)); + _paginationContext = paginationContext ?? throw new ArgumentNullException(nameof(paginationContext)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _traceWriter = new TraceLogWriter>(loggerFactory); + _request = request ?? throw new ArgumentNullException(nameof(request)); + _resourceChangeTracker = resourceChangeTracker ?? throw new ArgumentNullException(nameof(resourceChangeTracker)); + _resourceFactory = resourceFactory ?? throw new ArgumentNullException(nameof(resourceFactory)); _hookExecutor = hookExecutor; } + /// public virtual async Task CreateAsync(TResource resource) { - _logger.LogTrace($"Entering {nameof(CreateAsync)}(object)."); + _traceWriter.LogMethodStart(new {resource}); + if (resource == null) throw new ArgumentNullException(nameof(resource)); if (_hookExecutor != null) { @@ -75,9 +79,10 @@ public virtual async Task CreateAsync(TResource resource) return resource; } + /// public virtual async Task DeleteAsync(TId id) { - _logger.LogTrace($"Entering {nameof(DeleteAsync)}('{id}')."); + _traceWriter.LogMethodStart(new {id}); if (_hookExecutor != null) { @@ -103,9 +108,10 @@ public virtual async Task DeleteAsync(TId id) } } + /// public virtual async Task> GetAsync() { - _logger.LogTrace($"Entering {nameof(GetAsync)}()."); + _traceWriter.LogMethodStart(); _hookExecutor?.BeforeRead(ResourcePipeline.Get); @@ -115,21 +121,22 @@ public virtual async Task> GetAsync() _paginationContext.TotalResourceCount = await _repository.CountAsync(topFilter); } - var queryLayer = _queryLayerComposer.Compose(_currentRequest.PrimaryResource); + var queryLayer = _queryLayerComposer.Compose(_request.PrimaryResource); var resources = await _repository.GetAsync(queryLayer); if (_hookExecutor != null) { _hookExecutor.AfterRead(resources, ResourcePipeline.Get); - return _hookExecutor.OnReturn(resources, ResourcePipeline.Get).ToList(); + return _hookExecutor.OnReturn(resources, ResourcePipeline.Get).ToArray(); } return resources; } + /// public virtual async Task GetAsync(TId id) { - _logger.LogTrace($"Entering {nameof(GetAsync)}('{id}')."); + _traceWriter.LogMethodStart(new {id}); _hookExecutor?.BeforeRead(ResourcePipeline.GetSingle, id.ToString()); @@ -146,7 +153,7 @@ public virtual async Task GetAsync(TId id) private async Task GetPrimaryResourceById(TId id, bool allowTopSparseFieldSet) { - var primaryLayer = _queryLayerComposer.Compose(_currentRequest.PrimaryResource); + var primaryLayer = _queryLayerComposer.Compose(_request.PrimaryResource); primaryLayer.Sort = null; primaryLayer.Pagination = null; primaryLayer.Filter = CreateFilterById(id); @@ -171,24 +178,26 @@ private async Task GetPrimaryResourceById(TId id, bool allowTopSparse private FilterExpression CreateFilterById(TId id) { - var primaryIdAttribute = _currentRequest.PrimaryResource.Attributes.Single(a => a.Property.Name == nameof(Identifiable.Id)); + var primaryIdAttribute = _request.PrimaryResource.Attributes.Single(a => a.Property.Name == nameof(Identifiable.Id)); return new ComparisonExpression(ComparisonOperator.Equals, new ResourceFieldChainExpression(primaryIdAttribute), new LiteralConstantExpression(id.ToString())); } + /// // triggered by GET /articles/1/relationships/{relationshipName} public virtual async Task GetRelationshipAsync(TId id, string relationshipName) { - _logger.LogTrace($"Entering {nameof(GetRelationshipAsync)}('{id}', '{relationshipName}')."); + _traceWriter.LogMethodStart(new {id, relationshipName}); + if (relationshipName == null) throw new ArgumentNullException(nameof(relationshipName)); AssertRelationshipExists(relationshipName); _hookExecutor?.BeforeRead(ResourcePipeline.GetRelationship, id.ToString()); - var secondaryLayer = _queryLayerComposer.Compose(_currentRequest.SecondaryResource); + var secondaryLayer = _queryLayerComposer.Compose(_request.SecondaryResource); - var secondaryIdAttribute = _currentRequest.SecondaryResource.Attributes.Single(a => a.Property.Name == nameof(Identifiable.Id)); + var secondaryIdAttribute = _request.SecondaryResource.Attributes.Single(a => a.Property.Name == nameof(Identifiable.Id)); secondaryLayer.Include = null; secondaryLayer.Projection = new Dictionary @@ -212,16 +221,18 @@ public virtual async Task GetRelationshipAsync(TId id, string relatio return primaryResource; } + /// // triggered by GET /articles/1/{relationshipName} public virtual async Task GetSecondaryAsync(TId id, string relationshipName) { - _logger.LogTrace($"Entering {nameof(GetSecondaryAsync)}('{id}', '{relationshipName}')."); + _traceWriter.LogMethodStart(new {id, relationshipName}); + if (relationshipName == null) throw new ArgumentNullException(nameof(relationshipName)); AssertRelationshipExists(relationshipName); _hookExecutor?.BeforeRead(ResourcePipeline.GetRelationship, id.ToString()); - var secondaryLayer = _queryLayerComposer.Compose(_currentRequest.SecondaryResource); + var secondaryLayer = _queryLayerComposer.Compose(_request.SecondaryResource); var primaryLayer = GetPrimaryLayerForSecondaryEndpoint(secondaryLayer, id); var primaryResources = await _repository.GetAsync(primaryLayer); @@ -235,7 +246,7 @@ public virtual async Task GetSecondaryAsync(TId id, string relationshipN primaryResource = _hookExecutor.OnReturn(AsList(primaryResource), ResourcePipeline.GetRelationship).Single(); } - return _currentRequest.Relationship.GetValue(primaryResource); + return _request.Relationship.GetValue(primaryResource); } private QueryLayer GetPrimaryLayerForSecondaryEndpoint(QueryLayer secondaryLayer, TId primaryId) @@ -244,16 +255,16 @@ private QueryLayer GetPrimaryLayerForSecondaryEndpoint(QueryLayer secondaryLayer secondaryLayer.Include = null; var primaryIdAttribute = - _currentRequest.PrimaryResource.Attributes.Single(a => a.Property.Name == nameof(Identifiable.Id)); + _request.PrimaryResource.Attributes.Single(a => a.Property.Name == nameof(Identifiable.Id)); - return new QueryLayer(_currentRequest.PrimaryResource) + return new QueryLayer(_request.PrimaryResource) { Include = RewriteIncludeForSecondaryEndpoint(innerInclude), Filter = CreateFilterById(primaryId), Projection = new Dictionary { [primaryIdAttribute] = null, - [_currentRequest.Relationship] = secondaryLayer + [_request.Relationship] = secondaryLayer } }; } @@ -261,15 +272,17 @@ private QueryLayer GetPrimaryLayerForSecondaryEndpoint(QueryLayer secondaryLayer private IncludeExpression RewriteIncludeForSecondaryEndpoint(IncludeExpression relativeInclude) { var parentElement = relativeInclude != null - ? new IncludeElementExpression(_currentRequest.Relationship, relativeInclude.Elements) - : new IncludeElementExpression(_currentRequest.Relationship); + ? new IncludeElementExpression(_request.Relationship, relativeInclude.Elements) + : new IncludeElementExpression(_request.Relationship); return new IncludeExpression(new[] {parentElement}); } + /// public virtual async Task UpdateAsync(TId id, TResource requestResource) { - _logger.LogTrace($"Entering {nameof(UpdateAsync)}('{id}', {(requestResource == null ? "null" : "object")})."); + _traceWriter.LogMethodStart(new {id, requestResource}); + if (requestResource == null) throw new ArgumentNullException(nameof(requestResource)); TResource databaseResource = await GetPrimaryResourceById(id, false); @@ -297,14 +310,16 @@ public virtual async Task UpdateAsync(TId id, TResource requestResour return hasImplicitChanges ? afterResource : null; } + /// // triggered by PATCH /articles/1/relationships/{relationshipName} - public virtual async Task UpdateRelationshipAsync(TId id, string relationshipName, object related) + public virtual async Task UpdateRelationshipAsync(TId id, string relationshipName, object relationships) { - _logger.LogTrace($"Entering {nameof(UpdateRelationshipAsync)}('{id}', '{relationshipName}', {(related == null ? "null" : "object")})."); + _traceWriter.LogMethodStart(new {id, relationshipName, related = relationships}); + if (relationshipName == null) throw new ArgumentNullException(nameof(relationshipName)); AssertRelationshipExists(relationshipName); - var secondaryLayer = _queryLayerComposer.Compose(_currentRequest.SecondaryResource); + var secondaryLayer = _queryLayerComposer.Compose(_request.SecondaryResource); var primaryLayer = GetPrimaryLayerForSecondaryEndpoint(secondaryLayer, id); primaryLayer.Projection = null; @@ -319,14 +334,14 @@ public virtual async Task UpdateRelationshipAsync(TId id, string relationshipNam } string[] relationshipIds = null; - if (related != null) + if (relationships != null) { - relationshipIds = _currentRequest.Relationship is HasOneAttribute - ? new[] {((IIdentifiable) related).StringId} - : ((IEnumerable) related).Select(e => e.StringId).ToArray(); + relationshipIds = _request.Relationship is HasOneAttribute + ? new[] {((IIdentifiable) relationships).StringId} + : ((IEnumerable) relationships).Select(e => e.StringId).ToArray(); } - await _repository.UpdateRelationshipsAsync(primaryResource, _currentRequest.Relationship, relationshipIds ?? Array.Empty()); + await _repository.UpdateRelationshipsAsync(primaryResource, _request.Relationship, relationshipIds ?? Array.Empty()); if (_hookExecutor != null && primaryResource != null) { @@ -338,16 +353,16 @@ private void AssertPrimaryResourceExists(TResource resource) { if (resource == null) { - throw new ResourceNotFoundException(_currentRequest.PrimaryId, _currentRequest.PrimaryResource.ResourceName); + throw new ResourceNotFoundException(_request.PrimaryId, _request.PrimaryResource.ResourceName); } } private void AssertRelationshipExists(string relationshipName) { - var relationship = _currentRequest.Relationship; + var relationship = _request.Relationship; if (relationship == null) { - throw new RelationshipNotFoundException(relationshipName, _currentRequest.PrimaryResource.ResourceName); + throw new RelationshipNotFoundException(relationshipName, _request.PrimaryResource.ResourceName); } } @@ -358,9 +373,9 @@ private static List AsList(TResource resource) } /// - /// No mapping with integer as default + /// Represents the foundational Resource Service layer in the JsonApiDotNetCore architecture that uses a Resource Repository for data access. /// - /// + /// The resource type. public class JsonApiResourceService : JsonApiResourceService, IResourceService where TResource : class, IIdentifiable @@ -371,11 +386,11 @@ public JsonApiResourceService( IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, - ICurrentRequest currentRequest, + IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker, IResourceFactory resourceFactory, IResourceHookExecutor hookExecutor = null) - : base(repository, queryLayerComposer, paginationContext, options, loggerFactory, currentRequest, + : base(repository, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, resourceFactory, hookExecutor) { } } diff --git a/src/JsonApiDotNetCore/Services/ResourceDefinitionProvider.cs b/src/JsonApiDotNetCore/Services/ResourceDefinitionProvider.cs deleted file mode 100644 index eb85a9ccff..0000000000 --- a/src/JsonApiDotNetCore/Services/ResourceDefinitionProvider.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services.Contract; - -namespace JsonApiDotNetCore.Services -{ - /// - internal sealed class ResourceDefinitionProvider : IResourceDefinitionProvider - { - private readonly IResourceGraph _resourceContextProvider; - private readonly IScopedServiceProvider _serviceProvider; - - public ResourceDefinitionProvider(IResourceGraph resourceContextProvider, IScopedServiceProvider serviceProvider) - { - _resourceContextProvider = resourceContextProvider; - _serviceProvider = serviceProvider; - } - - /// - public IResourceDefinition Get(Type resourceType) - { - return (IResourceDefinition)_serviceProvider.GetService(_resourceContextProvider.GetResourceContext(resourceType).ResourceDefinitionType); - } - } -} diff --git a/src/JsonApiDotNetCore/Internal/TypeHelper.cs b/src/JsonApiDotNetCore/TypeHelper.cs similarity index 78% rename from src/JsonApiDotNetCore/Internal/TypeHelper.cs rename to src/JsonApiDotNetCore/TypeHelper.cs index 3792d2c5cb..348612bd5e 100644 --- a/src/JsonApiDotNetCore/Internal/TypeHelper.cs +++ b/src/JsonApiDotNetCore/TypeHelper.cs @@ -2,13 +2,12 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Linq.Expressions; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using System.Reflection; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.Internal +namespace JsonApiDotNetCore { internal static class TypeHelper { @@ -88,7 +87,7 @@ public static bool CanContainNull(Type type) return !type.IsValueType || Nullable.GetUnderlyingType(type) != null; } - internal static object GetDefaultValue(this Type type) + internal static object GetDefaultValue(Type type) { return type.IsValueType ? CreateInstance(type) : null; } @@ -99,7 +98,7 @@ public static Type TryGetCollectionElementType(Type type) { if (type.IsGenericType && type.GenericTypeArguments.Length == 1) { - if (type.IsOrImplementsInterface(typeof(IEnumerable))) + if (IsOrImplementsInterface(type, typeof(IEnumerable))) { return type.GenericTypeArguments[0]; } @@ -165,9 +164,7 @@ public static Dictionary> ConvertRelat /// /// Converts a dictionary of AttrAttributes to the underlying PropertyInfo that is referenced /// - /// - /// - public static Dictionary> ConvertAttributeDictionary(List attributes, HashSet resources) + public static Dictionary> ConvertAttributeDictionary(IEnumerable attributes, HashSet resources) { return attributes?.ToDictionary(attr => attr.Property, attr => resources); } @@ -181,7 +178,7 @@ public static Dictionary> ConvertAttributeDicti /// Open generic type public static object CreateInstanceOfOpenType(Type openType, Type parameter, params object[] constructorArguments) { - return CreateInstanceOfOpenType(openType, new[] { parameter }, constructorArguments); + return CreateInstanceOfOpenType(openType, new[] {parameter}, constructorArguments); } /// @@ -189,7 +186,7 @@ public static object CreateInstanceOfOpenType(Type openType, Type parameter, par /// public static object CreateInstanceOfOpenType(Type openType, Type parameter, bool hasInternalConstructor, params object[] constructorArguments) { - Type[] parameters = { parameter }; + Type[] parameters = {parameter}; if (!hasInternalConstructor) return CreateInstanceOfOpenType(openType, parameters, constructorArguments); var parameterizedType = openType.MakeGenericType(parameters); // note that if for whatever reason the constructor of AffectedResource is set from @@ -218,7 +215,7 @@ public static IEnumerable CreateHashSetFor(Type type, object elements = null) /// /// Returns a compatible collection type that can be instantiated, for example IList{Article} -> List{Article} or ISet{Article} -> HashSet{Article} /// - public static Type ToConcreteCollectionType(this Type collectionType) + public static Type ToConcreteCollectionType(Type collectionType) { if (collectionType.IsInterface && collectionType.IsGenericType) { @@ -280,11 +277,63 @@ public static object GetResourceTypedId(IIdentifiable resource) return property.GetValue(resource); } - public static string GetResourceStringId(TId id, IResourceFactory resourceFactory) where TResource : class, IIdentifiable + /// + /// Extension to use the LINQ cast method in a non-generic way: + /// + /// Type targetType = typeof(TResource) + /// ((IList)myList).CopyToList(targetType). + /// + /// + public static IList CopyToList(IEnumerable copyFrom, Type elementType, Converter elementConverter = null) { - TResource tempResource = resourceFactory.CreateInstance(); - tempResource.Id = id; - return tempResource.StringId; + Type collectionType = typeof(List<>).MakeGenericType(elementType); + + if (elementConverter != null) + { + var converted = copyFrom.Cast().Select(element => elementConverter(element)); + return (IList) CopyToTypedCollection(converted, collectionType); + } + + return (IList)CopyToTypedCollection(copyFrom, collectionType); + } + + /// + /// Creates a collection instance based on the specified collection type and copies the specified elements into it. + /// + /// Source to copy from. + /// Target collection type, for example: typeof(List{Article}) or typeof(ISet{Person}). + public static IEnumerable CopyToTypedCollection(IEnumerable source, Type collectionType) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + if (collectionType == null) throw new ArgumentNullException(nameof(collectionType)); + + var concreteCollectionType = ToConcreteCollectionType(collectionType); + dynamic concreteCollectionInstance = CreateInstance(concreteCollectionType); + + foreach (var item in source) + { + concreteCollectionInstance.Add((dynamic) item); + } + + return concreteCollectionInstance; + } + + /// + /// Whether the specified source type implements or equals the specified interface. + /// + public static bool IsOrImplementsInterface(Type source, Type interfaceType) + { + if (interfaceType == null) + { + throw new ArgumentNullException(nameof(interfaceType)); + } + + if (source == null) + { + return false; + } + + return source == interfaceType || source.GetInterfaces().Any(type => type == interfaceType); } } } diff --git a/src/JsonApiDotNetCore/index.md b/src/JsonApiDotNetCore/index.md deleted file mode 100644 index 3ae2506361..0000000000 --- a/src/JsonApiDotNetCore/index.md +++ /dev/null @@ -1,4 +0,0 @@ -# This is the **HOMEPAGE**. -Refer to [Markdown](http://daringfireball.net/projects/markdown/) for how to write markdown files. -## Quick Start Notes: -1. Add images to the *images* folder if the file is referencing an image. diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 6fae07413f..2d38877853 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -1,20 +1,12 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Graph; -using JsonApiDotNetCore.Hooks; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Internal.Generics; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Hooks.Internal; +using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.RequestServices; -using JsonApiDotNetCore.RequestServices.Contracts; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Serialization.Server.Builders; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Services; -using JsonApiDotNetCore.Services.Contract; using JsonApiDotNetCoreExample.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -41,7 +33,7 @@ public ServiceDiscoveryFacadeTests() _services.AddSingleton(options); _services.AddSingleton(new LoggerFactory()); _services.AddScoped(_ => new Mock().Object); - _services.AddScoped(_ => new Mock().Object); + _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); @@ -117,11 +109,11 @@ public TestModelService( IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, - ICurrentRequest currentRequest, + IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker, IResourceFactory resourceFactory, IResourceHookExecutor hookExecutor = null) - : base(repository, queryLayerComposer, paginationContext, options, loggerFactory, currentRequest, + : base(repository, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, resourceFactory, hookExecutor) { } diff --git a/test/IntegrationTests/Data/EntityFrameworkCoreRepositoryTests.cs b/test/IntegrationTests/Data/EntityFrameworkCoreRepositoryTests.cs index 50ac2e3c2a..09174695cd 100644 --- a/test/IntegrationTests/Data/EntityFrameworkCoreRepositoryTests.cs +++ b/test/IntegrationTests/Data/EntityFrameworkCoreRepositoryTests.cs @@ -1,24 +1,20 @@ -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCoreExample.Data; -using JsonApiDotNetCoreExample.Models; -using Microsoft.EntityFrameworkCore; -using Moq; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using IntegrationTests; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models.Annotation; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Logging.Abstractions; +using Moq; using Xunit; namespace JADNC.IntegrationTests.Data @@ -50,8 +46,9 @@ public async Task UpdateAsync_AttributesUpdated_ShouldHaveSpecificallyThoseAttri arrangeDbContext.Add(databaseResource); await arrangeDbContext.SaveChangesAsync(); - var descAttr = new AttrAttribute("description") + var descAttr = new AttrAttribute { + PublicName = "description", Property = typeof(TodoItem).GetProperty(nameof(TodoItem.Description)) }; targetedFields.Setup(m => m.Attributes).Returns(new List { descAttr }); @@ -89,9 +86,10 @@ public async Task UpdateAsync_AttributesUpdated_ShouldHaveSpecificallyThoseAttri var resourceFactory = new ResourceFactory(serviceProvider); var contextResolverMock = new Mock(); contextResolverMock.Setup(m => m.GetContext()).Returns(context); - var resourceGraph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).AddResource().Build(); + var resourceGraph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).Add().Build(); var targetedFields = new Mock(); - var repository = new EntityFrameworkCoreRepository(targetedFields.Object, contextResolverMock.Object, resourceGraph, null, resourceFactory, new List(), NullLoggerFactory.Instance); + var serviceFactory = new Mock().Object; + var repository = new EntityFrameworkCoreRepository(targetedFields.Object, contextResolverMock.Object, resourceGraph, serviceFactory, resourceFactory, new List(), NullLoggerFactory.Instance); return (repository, targetedFields, resourceGraph); } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ActionResultTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ActionResultTests.cs index b71ee6a6db..5fd33a87f5 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ActionResultTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ActionResultTests.cs @@ -3,8 +3,8 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using Newtonsoft.Json; using Xunit; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs index 3c764942f2..cdd5d448e4 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs @@ -4,8 +4,8 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using Bogus; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorHandlingTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorHandlingTests.cs index 09ede037d2..51f3568667 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorHandlingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorHandlingTests.cs @@ -1,9 +1,9 @@ using System; using System.Net; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Exceptions; +using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.Logging; using Xunit; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreDefaultValuesTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreDefaultValuesTests.cs index ebcd18f4a9..bc4400b7c1 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreDefaultValuesTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreDefaultValuesTests.cs @@ -3,8 +3,7 @@ using System.Net.Http; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -118,7 +117,7 @@ public async Task CheckBehaviorCombination(DefaultValueHandling? defaultValue, b var body = await response.Content.ReadAsStringAsync(); var isQueryStringValueEmpty = queryStringValue == string.Empty; - var isDisallowedOverride = options.AllowQueryStringOverrideForSerializerDefaultValueHandling == false && queryStringValue != null; + var isDisallowedOverride = !options.AllowQueryStringOverrideForSerializerDefaultValueHandling && queryStringValue != null; var isQueryStringInvalid = queryStringValue != null && !bool.TryParse(queryStringValue, out _); if (isQueryStringValueEmpty) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreNullValuesTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreNullValuesTests.cs index a2dc34e045..f6a03bf098 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreNullValuesTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreNullValuesTests.cs @@ -3,8 +3,7 @@ using System.Net.Http; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -121,7 +120,7 @@ public async Task CheckBehaviorCombination(NullValueHandling? defaultValue, bool var body = await response.Content.ReadAsStringAsync(); var isQueryStringValueEmpty = queryStringValue == string.Empty; - var isDisallowedOverride = options.AllowQueryStringOverrideForSerializerNullValueHandling == false && queryStringValue != null; + var isDisallowedOverride = !options.AllowQueryStringOverrideForSerializerNullValueHandling && queryStringValue != null; var isQueryStringInvalid = queryStringValue != null && !bool.TryParse(queryStringValue, out _); if (isQueryStringValueEmpty) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs index 0fd720a47d..46d3074325 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RequestMetaTests.cs @@ -1,13 +1,13 @@ +using System.Collections.Generic; using System.Net; using System.Threading.Tasks; -using Xunit; -using JsonApiDotNetCore.Models; -using System.Collections.Generic; using FluentAssertions; -using JsonApiDotNetCore.Services; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using Microsoft.Extensions.DependencyInjection; +using Xunit; namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility { @@ -45,7 +45,7 @@ public async Task Injecting_IRequestMeta_Adds_Meta_Data() public sealed class TestRequestMeta : IRequestMeta { - public Dictionary GetMeta() + public IReadOnlyDictionary GetMeta() { return new Dictionary { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs index 97187de056..16f660e3f6 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs @@ -1,7 +1,7 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpDeleteTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpDeleteTests.cs index e68da12e2c..7103dc3a2c 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpDeleteTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpDeleteTests.cs @@ -1,7 +1,7 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPatchTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPatchTests.cs index 034c81f9c9..7d26157c46 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPatchTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPatchTests.cs @@ -1,7 +1,7 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPostTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPostTests.cs index dae69ee497..23d63eca51 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPostTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/NoHttpPostTests.cs @@ -1,7 +1,7 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/InjectableResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/InjectableResourceTests.cs index ee009d4b25..1af223de4a 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/InjectableResourceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/InjectableResourceTests.cs @@ -3,8 +3,7 @@ using System.Net.Http; using System.Threading.Tasks; using Bogus; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -216,7 +215,7 @@ public async Task Fail_When_Deleting_Missing_Passport() Assert.Single(errorDocument.Errors); Assert.Equal(HttpStatusCode.NotFound, errorDocument.Errors[0].StatusCode); Assert.Equal("The requested resource does not exist.", errorDocument.Errors[0].Title); - Assert.Equal("Resource of type 'passports' with id '" + passportId + "' does not exist.", errorDocument.Errors[0].Detail); + Assert.Equal("Resource of type 'passports' with ID '" + passportId + "' does not exist.", errorDocument.Errors[0].Detail); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/KebabCaseFormatterTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/KebabCaseFormatterTests.cs index 6b936df648..49748e4f54 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/KebabCaseFormatterTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/KebabCaseFormatterTests.cs @@ -5,9 +5,9 @@ using Bogus; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization.Client; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Client.Internal; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs index 5aedc9992c..76d18461b7 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs @@ -5,8 +5,8 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using Bogus; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -78,7 +78,7 @@ public async Task Can_Fetch_Many_To_Many_Through_Id() var document = JsonConvert.DeserializeObject(body); Assert.Single(document.ManyData); - var tagResponse = _fixture.GetDeserializer().DeserializeList(body).Data.First(); + var tagResponse = _fixture.GetDeserializer().DeserializeMany(body).Data.First(); Assert.NotNull(tagResponse); Assert.Equal(tag.Id, tagResponse.Id); Assert.Equal(tag.Name, tagResponse.Name); @@ -117,7 +117,7 @@ public async Task Can_Fetch_Many_To_Many_Through_GetById_Relationship_Link() var document = JsonConvert.DeserializeObject(body); Assert.Null(document.Included); - var tagResponse = _fixture.GetDeserializer().DeserializeList(body).Data.First(); + var tagResponse = _fixture.GetDeserializer().DeserializeMany(body).Data.First(); Assert.NotNull(tagResponse); Assert.Equal(tag.Id, tagResponse.Id); } @@ -155,7 +155,7 @@ public async Task Can_Fetch_Many_To_Many_Through_Relationship_Link() var document = JsonConvert.DeserializeObject(body); Assert.Null(document.Included); - var tagResponse = _fixture.GetDeserializer().DeserializeList(body).Data.First(); + var tagResponse = _fixture.GetDeserializer().DeserializeMany(body).Data.First(); Assert.NotNull(tagResponse); Assert.Equal(tag.Id, tagResponse.Id); } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs index 66a6c7c14c..7a9d6f077b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs @@ -4,9 +4,9 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using Bogus; -using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCoreExampleTests.Acceptance.Spec; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs index 1ff0617237..6742ab6d80 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs @@ -5,10 +5,9 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using Bogus; -using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCoreExampleTests.Acceptance.Spec; using Microsoft.EntityFrameworkCore; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ContentNegotiationTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ContentNegotiationTests.cs index 18e03b03d3..b5bdac6497 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ContentNegotiationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ContentNegotiationTests.cs @@ -2,8 +2,8 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs index 5bfc0ecac9..5815d6ec90 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs @@ -3,7 +3,7 @@ using System.Net; using System.Threading.Tasks; using Bogus; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCoreExampleTests.Helpers.Models; using Microsoft.EntityFrameworkCore; @@ -81,7 +81,7 @@ public async Task ClientGeneratedId_IntegerIdAndNotEnabled_IsForbidden() var errorDocument = JsonConvert.DeserializeObject(body); Assert.Single(errorDocument.Errors); Assert.Equal(HttpStatusCode.Forbidden, errorDocument.Errors[0].StatusCode); - Assert.Equal("Specifying the resource id in POST requests is not allowed.", errorDocument.Errors[0].Title); + Assert.Equal("Specifying the resource ID in POST requests is not allowed.", errorDocument.Errors[0].Title); Assert.Null(errorDocument.Errors[0].Detail); } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeletingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeletingDataTests.cs index 810e673627..2a434f8508 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeletingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeletingDataTests.cs @@ -1,7 +1,7 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -51,7 +51,7 @@ public async Task Respond_404_If_ResourceDoesNotExist() Assert.Single(errorDocument.Errors); Assert.Equal(HttpStatusCode.NotFound, errorDocument.Errors[0].StatusCode); Assert.Equal("The requested resource does not exist.", errorDocument.Errors[0].Title); - Assert.Equal("Resource of type 'todoItems' with id '123' does not exist.",errorDocument.Errors[0].Detail); + Assert.Equal("Resource of type 'todoItems' with ID '123' does not exist.",errorDocument.Errors[0].Detail); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DisableQueryAttributeTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DisableQueryAttributeTests.cs index de1d3a6ff4..82fdfe32d7 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DisableQueryAttributeTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DisableQueryAttributeTests.cs @@ -1,7 +1,7 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using Newtonsoft.Json; using Xunit; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithNamespaceTests.cs index 644f22919c..e347d8097f 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithNamespaceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithNamespaceTests.cs @@ -2,7 +2,7 @@ using System.Net.Http; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample.Models; using Newtonsoft.Json; using Xunit; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithoutNamespaceTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithoutNamespaceTests.cs index 3f4a9cb994..9670d5003a 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithoutNamespaceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithoutNamespaceTests.cs @@ -1,15 +1,15 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; -using JsonApiDotNetCore.Models; -using Xunit; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Xunit; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec.DocumentTests { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs index ec03714fa7..65ce8d8f67 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs @@ -3,8 +3,9 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -120,7 +121,7 @@ public async Task Total_Resource_Count_Not_Included_In_POST_Response() // Assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); - Assert.True(documents.Meta?.ContainsKey("totalResources") != true); + Assert.True(documents.Meta == null || !documents.Meta.ContainsKey("totalResources")); } [Fact] @@ -163,7 +164,7 @@ public async Task Total_Resource_Count_Not_Included_In_PATCH_Response() // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.True(documents.Meta?.ContainsKey("totalResources") != true); + Assert.True(documents.Meta == null || !documents.Meta.ContainsKey("totalResources")); } [Fact] diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Relationships.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Relationships.cs index ba2395197c..cf183eb641 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Relationships.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Relationships.cs @@ -1,16 +1,16 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; +using Bogus; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Newtonsoft.Json; using Xunit; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCoreExample.Data; -using Bogus; -using JsonApiDotNetCoreExample.Models; -using Microsoft.AspNetCore; using Person = JsonApiDotNetCoreExample.Models.Person; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec.DocumentTests diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EagerLoadTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EagerLoadTests.cs index 2146efb130..cc6a9532b6 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EagerLoadTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EagerLoadTests.cs @@ -3,7 +3,7 @@ using System.Net; using System.Threading.Tasks; using Bogus; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Microsoft.Extensions.DependencyInjection; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs index 014d09bbca..bdaf4ee109 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs @@ -4,9 +4,9 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization.Client; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Client.Internal; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs index cbfb1051b2..8a1ba5b09b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs @@ -2,10 +2,9 @@ using System.Net.Http; using System.Threading.Tasks; using Bogus; -using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -57,7 +56,7 @@ public async Task Request_ForEmptyCollection_Returns_EmptyDataCollection() // Act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var result = _fixture.GetDeserializer().DeserializeList(body); + var result = _fixture.GetDeserializer().DeserializeMany(body); var items = result.Data; var meta = result.Meta; @@ -129,7 +128,7 @@ public async Task GetResources_NoDefaultPageSize_ReturnsResources() // Act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var result = _fixture.GetDeserializer().DeserializeList(body); + var result = _fixture.GetDeserializer().DeserializeMany(body); // Assert Assert.True(result.Data.Count == 20); @@ -162,7 +161,7 @@ public async Task GetSingleResource_ResourceDoesNotExist_ReturnsNotFound() Assert.Single(errorDocument.Errors); Assert.Equal(HttpStatusCode.NotFound, errorDocument.Errors[0].StatusCode); Assert.Equal("The requested resource does not exist.", errorDocument.Errors[0].Title); - Assert.Equal("Resource of type 'todoItems' with id '123' does not exist.", errorDocument.Errors[0].Detail); + Assert.Equal("Resource of type 'todoItems' with ID '123' does not exist.", errorDocument.Errors[0].Detail); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs index 93d860d03a..d351fc510e 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs @@ -3,8 +3,7 @@ using System.Net.Http; using System.Threading.Tasks; using Bogus; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -290,7 +289,7 @@ public async Task When_getting_related_for_missing_parent_resource_it_should_fai Assert.Single(errorDocument.Errors); Assert.Equal(HttpStatusCode.NotFound, errorDocument.Errors[0].StatusCode); Assert.Equal("The requested resource does not exist.", errorDocument.Errors[0].Title); - Assert.Equal("Resource of type 'todoItems' with id '99999999' does not exist.",errorDocument.Errors[0].Detail); + Assert.Equal("Resource of type 'todoItems' with ID '99999999' does not exist.",errorDocument.Errors[0].Detail); } [Fact] @@ -314,7 +313,7 @@ public async Task When_getting_relationship_for_missing_parent_resource_it_shoul Assert.Single(errorDocument.Errors); Assert.Equal(HttpStatusCode.NotFound, errorDocument.Errors[0].StatusCode); Assert.Equal("The requested resource does not exist.", errorDocument.Errors[0].Title); - Assert.Equal("Resource of type 'todoItems' with id '99999999' does not exist.",errorDocument.Errors[0].Detail); + Assert.Equal("Resource of type 'todoItems' with ID '99999999' does not exist.",errorDocument.Errors[0].Detail); } [Fact] diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FunctionalTestCollection.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FunctionalTestCollection.cs index c235dff461..870a2af9fa 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FunctionalTestCollection.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FunctionalTestCollection.cs @@ -4,14 +4,10 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Graph; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization.Client; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Client.Internal; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCoreExampleTests.Helpers.Models; @@ -78,10 +74,10 @@ protected IResponseDeserializer GetDeserializer() { continue; } - builder.AddResource(rc.ResourceType, rc.IdentityType, rc.ResourceName); + builder.Add(rc.ResourceType, rc.IdentityType, rc.ResourceName); } - builder.AddResource(formatter.FormatResourceName(typeof(TodoItem))); - builder.AddResource(formatter.FormatResourceName(typeof(TodoItemCollection))); + builder.Add(formatter.FormatResourceName(typeof(TodoItem))); + builder.Add(formatter.FormatResourceName(typeof(TodoItemCollection))); return new ResponseDeserializer(builder.Build(), new ResourceFactory(_factory.ServiceProvider)); } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PaginationLinkTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PaginationLinkTests.cs index 3755d50ff1..a1e38ad007 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PaginationLinkTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PaginationLinkTests.cs @@ -1,9 +1,8 @@ using System.Net; using System.Threading.Tasks; using Bogus; -using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample.Models; using Newtonsoft.Json; using Xunit; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ThrowingResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ThrowingResourceTests.cs index 852c9d6066..d597adc62c 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ThrowingResourceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ThrowingResourceTests.cs @@ -1,7 +1,7 @@ using System.Linq; using System.Net; using System.Threading.Tasks; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample.Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs index 8cb02aed4e..117c4cb464 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs @@ -5,10 +5,9 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using Bogus; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Formatters; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -154,7 +153,7 @@ public async Task Respond_404_If_ResourceDoesNotExist() Assert.Single(errorDocument.Errors); Assert.Equal(HttpStatusCode.NotFound, errorDocument.Errors[0].StatusCode); Assert.Equal("The requested resource does not exist.", errorDocument.Errors[0].Title); - Assert.Equal("Resource of type 'todoItems' with id '100' does not exist.", errorDocument.Errors[0].Detail); + Assert.Equal("Resource of type 'todoItems' with ID '100' does not exist.", errorDocument.Errors[0].Detail); } [Fact] @@ -185,7 +184,7 @@ public async Task Respond_422_If_IdNotInAttributeList() var error = document.Errors.Single(); Assert.Equal(HttpStatusCode.UnprocessableEntity, error.StatusCode); - Assert.Equal("Failed to deserialize request body: Payload must include id attribute.", error.Title); + Assert.Equal("Failed to deserialize request body: Payload must include 'id' element.", error.Title); Assert.StartsWith("Request body: <<", error.Detail); } @@ -220,8 +219,8 @@ public async Task Respond_409_If_IdInUrlIsDifferentFromIdInRequestBody() var error = document.Errors.Single(); Assert.Equal(HttpStatusCode.Conflict, error.StatusCode); - Assert.Equal("Resource id mismatch between request body and endpoint URL.", error.Title); - Assert.Equal($"Expected resource id '{wrongTodoItemId}' in PATCH request body at endpoint 'http://localhost/api/v1/todoItems/{wrongTodoItemId}', instead of '{todoItem.Id}'.", error.Detail); + Assert.Equal("Resource ID mismatch between request body and endpoint URL.", error.Title); + Assert.Equal($"Expected resource ID '{wrongTodoItemId}' in PATCH request body at endpoint 'http://localhost/api/v1/todoItems/{wrongTodoItemId}', instead of '{todoItem.Id}'.", error.Detail); } [Fact] diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs index be48cdfde5..52cda9df1b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs @@ -5,8 +5,8 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using Bogus; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -844,7 +844,7 @@ public async Task Fails_On_Missing_Resource() Assert.Single(errorDocument.Errors); Assert.Equal(HttpStatusCode.NotFound, errorDocument.Errors[0].StatusCode); Assert.Equal("The requested resource does not exist.", errorDocument.Errors[0].Title); - Assert.Equal("Resource of type 'todoItems' with id '99999999' does not exist.",errorDocument.Errors[0].Detail); + Assert.Equal("Resource of type 'todoItems' with ID '99999999' does not exist.",errorDocument.Errors[0].Detail); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs index f4f3066c1d..b9e35a4ca2 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs @@ -2,13 +2,10 @@ using System.Linq.Expressions; using System.Net; using System.Net.Http; -using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization.Client; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Client.Internal; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCoreExampleTests.Helpers.Models; @@ -63,17 +60,17 @@ public IResponseDeserializer GetDeserializer() var options = GetService(); var resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance) - .AddResource() - .AddResource
() - .AddResource() - .AddResource() - .AddResource() - .AddResource() - .AddResource() - .AddResource() - .AddResource() - .AddResource("todoItems") - .AddResource().Build(); + .Add() + .Add
() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add("todoItems") + .Add().Build(); return new ResponseDeserializer(resourceGraph, new ResourceFactory(ServiceProvider)); } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemControllerTests.cs index 30adb44a6d..b4b03d5a65 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemControllerTests.cs @@ -6,9 +6,9 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using Bogus; -using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -68,7 +68,7 @@ public async Task Can_Get_TodoItems_Paginate_Check() // Act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetDeserializer().DeserializeList(body).Data; + var deserializedBody = _fixture.GetDeserializer().DeserializeMany(body).Data; // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs b/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs index 4407e43642..9496c0394b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs +++ b/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs @@ -1,7 +1,7 @@ -using JsonApiDotNetCore; +using System.Reflection; +using JsonApiDotNetCore.Configuration; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; -using System.Reflection; namespace JsonApiDotNetCoreExampleTests { diff --git a/test/JsonApiDotNetCoreExampleTests/Factories/CustomApplicationFactoryBase.cs b/test/JsonApiDotNetCoreExampleTests/Factories/CustomApplicationFactoryBase.cs index 9497a3e123..d9d1fb8fb5 100644 --- a/test/JsonApiDotNetCoreExampleTests/Factories/CustomApplicationFactoryBase.cs +++ b/test/JsonApiDotNetCoreExampleTests/Factories/CustomApplicationFactoryBase.cs @@ -1,9 +1,9 @@ using System; +using System.Net.Http; using JsonApiDotNetCoreExample; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; -using System.Net.Http; -using Microsoft.AspNetCore.Hosting; namespace JsonApiDotNetCoreExampleTests { diff --git a/test/JsonApiDotNetCoreExampleTests/Factories/ResourceHooksApplicationFactory.cs b/test/JsonApiDotNetCoreExampleTests/Factories/ResourceHooksApplicationFactory.cs index 5861a3f63b..b38c58be59 100644 --- a/test/JsonApiDotNetCoreExampleTests/Factories/ResourceHooksApplicationFactory.cs +++ b/test/JsonApiDotNetCoreExampleTests/Factories/ResourceHooksApplicationFactory.cs @@ -1,5 +1,5 @@ using System.Reflection; -using JsonApiDotNetCore; +using JsonApiDotNetCore.Configuration; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; diff --git a/test/JsonApiDotNetCoreExampleTests/Factories/StandardApplicationFactory.cs b/test/JsonApiDotNetCoreExampleTests/Factories/StandardApplicationFactory.cs index 8915bdf014..8c45e2e5e7 100644 --- a/test/JsonApiDotNetCoreExampleTests/Factories/StandardApplicationFactory.cs +++ b/test/JsonApiDotNetCoreExampleTests/Factories/StandardApplicationFactory.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore; +using JsonApiDotNetCore.Configuration; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Models/TodoItemClient.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Models/TodoItemClient.cs index e8db5d53f5..8dd88c4633 100644 --- a/test/JsonApiDotNetCoreExampleTests/Helpers/Models/TodoItemClient.cs +++ b/test/JsonApiDotNetCoreExampleTests/Helpers/Models/TodoItemClient.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCoreExample.Models; namespace JsonApiDotNetCoreExampleTests.Helpers.Models diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTestContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTestContext.cs index b96db01b3f..70e7136cd7 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTestContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTestContext.cs @@ -2,7 +2,7 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; -using JsonApiDotNetCore; +using JsonApiDotNetCore.Middleware; using JsonApiDotNetCoreExample; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs index eb6c6c1320..1cb8987091 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterDataTypeTests.cs @@ -5,8 +5,7 @@ using FluentAssertions.Extensions; using Humanizer; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using Xunit; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterDepthTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterDepthTests.cs index eb07d639b5..e497ab68dd 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterDepthTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterDepthTests.cs @@ -5,8 +5,7 @@ using FluentAssertions; using FluentAssertions.Extensions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs index 3ca1f69ce0..792de65335 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterOperatorTests.cs @@ -6,9 +6,8 @@ using FluentAssertions; using Humanizer; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using Xunit; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterTests.cs index 64c85131d1..43a105b644 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterTests.cs @@ -3,8 +3,7 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterableResource.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterableResource.cs index caeb240fc1..be66eec1de 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterableResource.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterableResource.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Filtering { diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterableResourcesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterableResourcesController.cs index cc28101ac4..7314d155d0 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterableResourcesController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Filtering/FilterableResourcesController.cs @@ -7,9 +7,9 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Filtering { public sealed class FilterableResourcesController : JsonApiController { - public FilterableResourcesController(IJsonApiOptions jsonApiOptions, ILoggerFactory loggerFactory, + public FilterableResourcesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Includes/IncludeTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Includes/IncludeTests.cs index 952ca23b2b..97f7c15dd9 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Includes/IncludeTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Includes/IncludeTests.cs @@ -5,8 +5,7 @@ using FluentAssertions; using FluentAssertions.Extensions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Pagination/PaginationRangeTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Pagination/PaginationRangeTests.cs index 7ac1706401..82075768d9 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Pagination/PaginationRangeTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Pagination/PaginationRangeTests.cs @@ -2,10 +2,8 @@ using System.Threading.Tasks; using Bogus; using FluentAssertions; -using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Pagination/PaginationRangeWithMaximumTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Pagination/PaginationRangeWithMaximumTests.cs index 3d391c5b89..6e6901a0e6 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Pagination/PaginationRangeWithMaximumTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Pagination/PaginationRangeWithMaximumTests.cs @@ -1,10 +1,8 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; -using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using Microsoft.Extensions.DependencyInjection; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Pagination/PaginationTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Pagination/PaginationTests.cs index 988be680d0..aecee57f26 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Pagination/PaginationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Pagination/PaginationTests.cs @@ -4,10 +4,8 @@ using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Extensions; -using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/QueryStringTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/QueryStringTests.cs index d523435fcc..6241847a31 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/QueryStringTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/QueryStringTests.cs @@ -2,8 +2,7 @@ using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using Microsoft.Extensions.DependencyInjection; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResource.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResource.cs index d645448a7d..e63ae24daa 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResource.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResource.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions { @@ -19,13 +19,13 @@ public sealed class CallableResource : Identifiable [Attr] public int RiskLevel { get; set; } - [Attr(AttrCapabilities.AllowView | AttrCapabilities.AllowSort)] + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowSort)] public DateTime CreatedAt { get; set; } - [Attr(AttrCapabilities.AllowView | AttrCapabilities.AllowSort)] + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowSort)] public DateTime ModifiedAt { get; set; } - [Attr(AttrCapabilities.None)] + [Attr(Capabilities = AttrCapabilities.None)] public bool IsDeleted { get; set; } [HasMany] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResourceDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResourceDefinition.cs index c4f8d429c7..87bef32537 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResourceDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResourceDefinition.cs @@ -2,12 +2,11 @@ using System.ComponentModel; using System.Linq; using System.Net; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.Primitives; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResourcesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResourcesController.cs index 128bf81d06..fba77b6b8f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResourcesController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/CallableResourcesController.cs @@ -7,9 +7,9 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions { public sealed class CallableResourcesController : JsonApiController { - public CallableResourcesController(IJsonApiOptions jsonApiOptions, ILoggerFactory loggerFactory, + public CallableResourcesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs index 2bed4e9e7c..e9a27ebda4 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/ResourceDefinitionQueryCallbackTests.cs @@ -3,8 +3,8 @@ using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Extensions; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using Xunit; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Sorting/SortTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Sorting/SortTests.cs index 52aa6e31ee..5ec7869582 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Sorting/SortTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Sorting/SortTests.cs @@ -5,8 +5,7 @@ using Bogus; using FluentAssertions; using FluentAssertions.Extensions; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SparseFieldSets/ResourceCaptureStore.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SparseFieldSets/ResourceCaptureStore.cs index dd026f6f86..a4cef25a9f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SparseFieldSets/ResourceCaptureStore.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SparseFieldSets/ResourceCaptureStore.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Resources; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.SparseFieldSets { diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs index b9edcc9545..557834c518 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SparseFieldSets/ResultCapturingRepository.cs @@ -1,12 +1,9 @@ using System.Collections.Generic; using System.Threading.Tasks; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Internal.Generics; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Resources; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.SparseFieldSets diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs index aa161ccb29..97a7d1b117 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs @@ -5,9 +5,8 @@ using Bogus; using FluentAssertions; using FluentAssertions.Extensions; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/TestableStartup.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/TestableStartup.cs index aac419c83a..ba110a7399 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/TestableStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/TestableStartup.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCoreExample; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; diff --git a/test/NoEntityFrameworkTests/WorkItemTests.cs b/test/NoEntityFrameworkTests/WorkItemTests.cs index 2bb2edc35f..decaf0e837 100644 --- a/test/NoEntityFrameworkTests/WorkItemTests.cs +++ b/test/NoEntityFrameworkTests/WorkItemTests.cs @@ -1,16 +1,16 @@ -using JsonApiDotNetCore; -using JsonApiDotNetCore.Models; +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using NoEntityFrameworkExample; using NoEntityFrameworkExample.Data; using NoEntityFrameworkExample.Models; -using System; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; using Xunit; namespace NoEntityFrameworkTests diff --git a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs index ab5d9f8dc2..22c9b9e927 100644 --- a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs +++ b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs @@ -1,12 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; @@ -38,7 +35,7 @@ public void Can_Build_ResourceGraph_Using_Builder() services.AddLogging(); services.AddDbContext(); - services.AddJsonApi(resources: builder => builder.AddResource("nonDbResources")); + services.AddJsonApi(resources: builder => builder.Add("nonDbResources")); // Act var container = services.BuildServiceProvider(); @@ -57,7 +54,7 @@ public void Resources_Without_Names_Specified_Will_Use_Configured_Formatter() { // Arrange var builder = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance); - builder.AddResource(); + builder.Add(); // Act var resourceGraph = builder.Build(); @@ -72,7 +69,7 @@ public void Attrs_Without_Names_Specified_Will_Use_Configured_Formatter() { // Arrange var builder = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance); - builder.AddResource(); + builder.Add(); // Act var resourceGraph = builder.Build(); @@ -87,7 +84,7 @@ public void Relationships_Without_Names_Specified_Will_Use_Configured_Formatter( { // Arrange var builder = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance); - builder.AddResource(); + builder.Add(); // Act var resourceGraph = builder.Build(); diff --git a/test/UnitTests/Builders/LinkBuilderTests.cs b/test/UnitTests/Builders/LinkBuilderTests.cs index fdeec4eabc..21f90d062b 100644 --- a/test/UnitTests/Builders/LinkBuilderTests.cs +++ b/test/UnitTests/Builders/LinkBuilderTests.cs @@ -1,17 +1,14 @@ -using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.QueryStrings; -using JsonApiDotNetCore.RequestServices.Contracts; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCoreExample.Models; -using Moq; -using Xunit; -using JsonApiDotNetCore.Serialization.Server.Builders; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; +using Moq; +using Xunit; namespace UnitTests { @@ -36,22 +33,22 @@ public LinkBuilderTests() } [Theory] - [InlineData(Links.All, Links.NotConfigured, _resourceSelf)] - [InlineData(Links.Self, Links.NotConfigured, _resourceSelf)] - [InlineData(Links.None, Links.NotConfigured, null)] - [InlineData(Links.All, Links.Self, _resourceSelf)] - [InlineData(Links.Self, Links.Self, _resourceSelf)] - [InlineData(Links.None, Links.Self, _resourceSelf)] - [InlineData(Links.All, Links.None, null)] - [InlineData(Links.Self, Links.None, null)] - [InlineData(Links.None, Links.None, null)] - public void BuildResourceLinks_GlobalAndResourceConfiguration_ExpectedResult(Links global, Links resource, object expectedResult) + [InlineData(LinkTypes.All, LinkTypes.NotConfigured, _resourceSelf)] + [InlineData(LinkTypes.Self, LinkTypes.NotConfigured, _resourceSelf)] + [InlineData(LinkTypes.None, LinkTypes.NotConfigured, null)] + [InlineData(LinkTypes.All, LinkTypes.Self, _resourceSelf)] + [InlineData(LinkTypes.Self, LinkTypes.Self, _resourceSelf)] + [InlineData(LinkTypes.None, LinkTypes.Self, _resourceSelf)] + [InlineData(LinkTypes.All, LinkTypes.None, null)] + [InlineData(LinkTypes.Self, LinkTypes.None, null)] + [InlineData(LinkTypes.None, LinkTypes.None, null)] + public void BuildResourceLinks_GlobalAndResourceConfiguration_ExpectedResult(LinkTypes global, LinkTypes resource, object expectedResult) { // Arrange var config = GetConfiguration(resourceLinks: global); var primaryResource = GetArticleResourceContext(resourceLinks: resource); _provider.Setup(m => m.GetResourceContext("articles")).Returns(primaryResource); - var builder = new LinkBuilder(config, GetRequestManager(), null, _provider.Object, _queryStringAccessor); + var builder = new LinkBuilder(config, GetRequestManager(), new PaginationContext(), _provider.Object, _queryStringAccessor); // Act var links = builder.GetResourceLinks("articles", _primaryId.ToString()); @@ -64,40 +61,40 @@ public void BuildResourceLinks_GlobalAndResourceConfiguration_ExpectedResult(Lin } [Theory] - [InlineData(Links.All, Links.NotConfigured, Links.NotConfigured, _relSelf, _relRelated)] - [InlineData(Links.All, Links.NotConfigured, Links.All, _relSelf, _relRelated)] - [InlineData(Links.All, Links.NotConfigured, Links.Self, _relSelf, null)] - [InlineData(Links.All, Links.NotConfigured, Links.Related, null, _relRelated)] - [InlineData(Links.All, Links.NotConfigured, Links.None, null, null)] - [InlineData(Links.All, Links.All, Links.NotConfigured, _relSelf, _relRelated)] - [InlineData(Links.All, Links.All, Links.All, _relSelf, _relRelated)] - [InlineData(Links.All, Links.All, Links.Self, _relSelf, null)] - [InlineData(Links.All, Links.All, Links.Related, null, _relRelated)] - [InlineData(Links.All, Links.All, Links.None, null, null)] - [InlineData(Links.All, Links.Self, Links.NotConfigured, _relSelf, null)] - [InlineData(Links.All, Links.Self, Links.All, _relSelf, _relRelated)] - [InlineData(Links.All, Links.Self, Links.Self, _relSelf, null)] - [InlineData(Links.All, Links.Self, Links.Related, null, _relRelated)] - [InlineData(Links.All, Links.Self, Links.None, null, null)] - [InlineData(Links.All, Links.Related, Links.NotConfigured, null, _relRelated)] - [InlineData(Links.All, Links.Related, Links.All, _relSelf, _relRelated)] - [InlineData(Links.All, Links.Related, Links.Self, _relSelf, null)] - [InlineData(Links.All, Links.Related, Links.Related, null, _relRelated)] - [InlineData(Links.All, Links.Related, Links.None, null, null)] - [InlineData(Links.All, Links.None, Links.NotConfigured, null, null)] - [InlineData(Links.All, Links.None, Links.All, _relSelf, _relRelated)] - [InlineData(Links.All, Links.None, Links.Self, _relSelf, null)] - [InlineData(Links.All, Links.None, Links.Related, null, _relRelated)] - [InlineData(Links.All, Links.None, Links.None, null, null)] + [InlineData(LinkTypes.All, LinkTypes.NotConfigured, LinkTypes.NotConfigured, _relSelf, _relRelated)] + [InlineData(LinkTypes.All, LinkTypes.NotConfigured, LinkTypes.All, _relSelf, _relRelated)] + [InlineData(LinkTypes.All, LinkTypes.NotConfigured, LinkTypes.Self, _relSelf, null)] + [InlineData(LinkTypes.All, LinkTypes.NotConfigured, LinkTypes.Related, null, _relRelated)] + [InlineData(LinkTypes.All, LinkTypes.NotConfigured, LinkTypes.None, null, null)] + [InlineData(LinkTypes.All, LinkTypes.All, LinkTypes.NotConfigured, _relSelf, _relRelated)] + [InlineData(LinkTypes.All, LinkTypes.All, LinkTypes.All, _relSelf, _relRelated)] + [InlineData(LinkTypes.All, LinkTypes.All, LinkTypes.Self, _relSelf, null)] + [InlineData(LinkTypes.All, LinkTypes.All, LinkTypes.Related, null, _relRelated)] + [InlineData(LinkTypes.All, LinkTypes.All, LinkTypes.None, null, null)] + [InlineData(LinkTypes.All, LinkTypes.Self, LinkTypes.NotConfigured, _relSelf, null)] + [InlineData(LinkTypes.All, LinkTypes.Self, LinkTypes.All, _relSelf, _relRelated)] + [InlineData(LinkTypes.All, LinkTypes.Self, LinkTypes.Self, _relSelf, null)] + [InlineData(LinkTypes.All, LinkTypes.Self, LinkTypes.Related, null, _relRelated)] + [InlineData(LinkTypes.All, LinkTypes.Self, LinkTypes.None, null, null)] + [InlineData(LinkTypes.All, LinkTypes.Related, LinkTypes.NotConfigured, null, _relRelated)] + [InlineData(LinkTypes.All, LinkTypes.Related, LinkTypes.All, _relSelf, _relRelated)] + [InlineData(LinkTypes.All, LinkTypes.Related, LinkTypes.Self, _relSelf, null)] + [InlineData(LinkTypes.All, LinkTypes.Related, LinkTypes.Related, null, _relRelated)] + [InlineData(LinkTypes.All, LinkTypes.Related, LinkTypes.None, null, null)] + [InlineData(LinkTypes.All, LinkTypes.None, LinkTypes.NotConfigured, null, null)] + [InlineData(LinkTypes.All, LinkTypes.None, LinkTypes.All, _relSelf, _relRelated)] + [InlineData(LinkTypes.All, LinkTypes.None, LinkTypes.Self, _relSelf, null)] + [InlineData(LinkTypes.All, LinkTypes.None, LinkTypes.Related, null, _relRelated)] + [InlineData(LinkTypes.All, LinkTypes.None, LinkTypes.None, null, null)] public void BuildRelationshipLinks_GlobalResourceAndAttrConfiguration_ExpectedLinks( - Links global, Links resource, Links relationship, object expectedSelfLink, object expectedRelatedLink) + LinkTypes global, LinkTypes resource, LinkTypes relationship, object expectedSelfLink, object expectedRelatedLink) { // Arrange var config = GetConfiguration(relationshipLinks: global); var primaryResource = GetArticleResourceContext(relationshipLinks: resource); _provider.Setup(m => m.GetResourceContext(typeof(Article))).Returns(primaryResource); - var builder = new LinkBuilder(config, GetRequestManager(), null, _provider.Object, _queryStringAccessor); - var attr = new HasOneAttribute(links: relationship) { RightType = typeof(Author), PublicName = "author" }; + var builder = new LinkBuilder(config, GetRequestManager(), new PaginationContext(), _provider.Object, _queryStringAccessor); + var attr = new HasOneAttribute { Links = relationship, RightType = typeof(Author), PublicName = "author" }; // Act var links = builder.GetRelationshipLinks(attr, new Article { Id = _primaryId }); @@ -115,36 +112,36 @@ public void BuildRelationshipLinks_GlobalResourceAndAttrConfiguration_ExpectedLi } [Theory] - [InlineData(Links.All, Links.NotConfigured, _topSelf, true)] - [InlineData(Links.All, Links.All, _topSelf, true)] - [InlineData(Links.All, Links.Self, _topSelf, false)] - [InlineData(Links.All, Links.Paging, null, true)] - [InlineData(Links.All, Links.None, null, false)] - [InlineData(Links.Self, Links.NotConfigured, _topSelf, false)] - [InlineData(Links.Self, Links.All, _topSelf, true)] - [InlineData(Links.Self, Links.Self, _topSelf, false)] - [InlineData(Links.Self, Links.Paging, null, true)] - [InlineData(Links.Self, Links.None, null, false)] - [InlineData(Links.Paging, Links.NotConfigured, null, true)] - [InlineData(Links.Paging, Links.All, _topSelf, true)] - [InlineData(Links.Paging, Links.Self, _topSelf, false)] - [InlineData(Links.Paging, Links.Paging, null, true)] - [InlineData(Links.Paging, Links.None, null, false)] - [InlineData(Links.None, Links.NotConfigured, null, false)] - [InlineData(Links.None, Links.All, _topSelf, true)] - [InlineData(Links.None, Links.Self, _topSelf, false)] - [InlineData(Links.None, Links.Paging, null, true)] - [InlineData(Links.None, Links.None, null, false)] - [InlineData(Links.All, Links.Self, _topResourceSelf, false)] - [InlineData(Links.Self, Links.Self, _topResourceSelf, false)] - [InlineData(Links.Paging, Links.Self, _topResourceSelf, false)] - [InlineData(Links.None, Links.Self, _topResourceSelf, false)] - [InlineData(Links.All, Links.Self, _topRelatedSelf, false)] - [InlineData(Links.Self, Links.Self, _topRelatedSelf, false)] - [InlineData(Links.Paging, Links.Self, _topRelatedSelf, false)] - [InlineData(Links.None, Links.Self, _topRelatedSelf, false)] + [InlineData(LinkTypes.All, LinkTypes.NotConfigured, _topSelf, true)] + [InlineData(LinkTypes.All, LinkTypes.All, _topSelf, true)] + [InlineData(LinkTypes.All, LinkTypes.Self, _topSelf, false)] + [InlineData(LinkTypes.All, LinkTypes.Paging, null, true)] + [InlineData(LinkTypes.All, LinkTypes.None, null, false)] + [InlineData(LinkTypes.Self, LinkTypes.NotConfigured, _topSelf, false)] + [InlineData(LinkTypes.Self, LinkTypes.All, _topSelf, true)] + [InlineData(LinkTypes.Self, LinkTypes.Self, _topSelf, false)] + [InlineData(LinkTypes.Self, LinkTypes.Paging, null, true)] + [InlineData(LinkTypes.Self, LinkTypes.None, null, false)] + [InlineData(LinkTypes.Paging, LinkTypes.NotConfigured, null, true)] + [InlineData(LinkTypes.Paging, LinkTypes.All, _topSelf, true)] + [InlineData(LinkTypes.Paging, LinkTypes.Self, _topSelf, false)] + [InlineData(LinkTypes.Paging, LinkTypes.Paging, null, true)] + [InlineData(LinkTypes.Paging, LinkTypes.None, null, false)] + [InlineData(LinkTypes.None, LinkTypes.NotConfigured, null, false)] + [InlineData(LinkTypes.None, LinkTypes.All, _topSelf, true)] + [InlineData(LinkTypes.None, LinkTypes.Self, _topSelf, false)] + [InlineData(LinkTypes.None, LinkTypes.Paging, null, true)] + [InlineData(LinkTypes.None, LinkTypes.None, null, false)] + [InlineData(LinkTypes.All, LinkTypes.Self, _topResourceSelf, false)] + [InlineData(LinkTypes.Self, LinkTypes.Self, _topResourceSelf, false)] + [InlineData(LinkTypes.Paging, LinkTypes.Self, _topResourceSelf, false)] + [InlineData(LinkTypes.None, LinkTypes.Self, _topResourceSelf, false)] + [InlineData(LinkTypes.All, LinkTypes.Self, _topRelatedSelf, false)] + [InlineData(LinkTypes.Self, LinkTypes.Self, _topRelatedSelf, false)] + [InlineData(LinkTypes.Paging, LinkTypes.Self, _topRelatedSelf, false)] + [InlineData(LinkTypes.None, LinkTypes.Self, _topRelatedSelf, false)] public void BuildTopLevelLinks_GlobalAndResourceConfiguration_ExpectedLinks( - Links global, Links resource, string expectedSelfLink, bool pages) + LinkTypes global, LinkTypes resource, string expectedSelfLink, bool pages) { // Arrange var config = GetConfiguration(topLevelLinks: global); @@ -153,9 +150,9 @@ public void BuildTopLevelLinks_GlobalAndResourceConfiguration_ExpectedLinks( bool usePrimaryId = expectedSelfLink != _topSelf; string relationshipName = expectedSelfLink == _topRelatedSelf ? _relationshipName : null; - ICurrentRequest currentRequest = GetRequestManager(primaryResource, usePrimaryId, relationshipName); + IJsonApiRequest request = GetRequestManager(primaryResource, usePrimaryId, relationshipName); - var builder = new LinkBuilder(config, currentRequest, _paginationContext, _provider.Object, _queryStringAccessor); + var builder = new LinkBuilder(config, request, _paginationContext, _provider.Object, _queryStringAccessor); // Act var links = builder.GetTopLevelLinks(); @@ -186,17 +183,17 @@ public void BuildTopLevelLinks_GlobalAndResourceConfiguration_ExpectedLinks( } } - private ICurrentRequest GetRequestManager(ResourceContext resourceContext = null, bool usePrimaryId = false, string relationshipName = null) + private IJsonApiRequest GetRequestManager(ResourceContext resourceContext = null, bool usePrimaryId = false, string relationshipName = null) { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(m => m.BasePath).Returns(_host); mock.Setup(m => m.PrimaryId).Returns(usePrimaryId ? _primaryId.ToString() : null); - mock.Setup(m => m.Relationship).Returns(relationshipName != null ? new HasOneAttribute(relationshipName) : null); + mock.Setup(m => m.Relationship).Returns(relationshipName != null ? new HasOneAttribute {PublicName = relationshipName} : null); mock.Setup(m => m.PrimaryResource).Returns(resourceContext); return mock.Object; } - private IJsonApiOptions GetConfiguration(Links resourceLinks = Links.All, Links topLevelLinks = Links.All, Links relationshipLinks = Links.All) + private IJsonApiOptions GetConfiguration(LinkTypes resourceLinks = LinkTypes.All, LinkTypes topLevelLinks = LinkTypes.All, LinkTypes relationshipLinks = LinkTypes.All) { var config = new Mock(); config.Setup(m => m.TopLevelLinks).Returns(topLevelLinks); @@ -216,9 +213,9 @@ private IPaginationContext GetPaginationContext() return mock.Object; } - private ResourceContext GetArticleResourceContext(Links resourceLinks = Links.NotConfigured, - Links topLevelLinks = Links.NotConfigured, - Links relationshipLinks = Links.NotConfigured) + private ResourceContext GetArticleResourceContext(LinkTypes resourceLinks = LinkTypes.NotConfigured, + LinkTypes topLevelLinks = LinkTypes.NotConfigured, + LinkTypes relationshipLinks = LinkTypes.NotConfigured) { return new ResourceContext { diff --git a/test/UnitTests/Builders/LinkTests.cs b/test/UnitTests/Builders/LinkTests.cs index 1489074277..1cf5501ec0 100644 --- a/test/UnitTests/Builders/LinkTests.cs +++ b/test/UnitTests/Builders/LinkTests.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Resources.Annotations; using Xunit; namespace UnitTests.Builders @@ -6,31 +6,31 @@ namespace UnitTests.Builders public sealed class LinkTests { [Theory] - [InlineData(Links.All, Links.Self, true)] - [InlineData(Links.All, Links.Related, true)] - [InlineData(Links.All, Links.Paging, true)] - [InlineData(Links.None, Links.Self, false)] - [InlineData(Links.None, Links.Related, false)] - [InlineData(Links.None, Links.Paging, false)] - [InlineData(Links.NotConfigured, Links.Self, false)] - [InlineData(Links.NotConfigured, Links.Related, false)] - [InlineData(Links.NotConfigured, Links.Paging, false)] - [InlineData(Links.Self, Links.Self, true)] - [InlineData(Links.Self, Links.Related, false)] - [InlineData(Links.Self, Links.Paging, false)] - [InlineData(Links.Self, Links.None, false)] - [InlineData(Links.Self, Links.NotConfigured, false)] - [InlineData(Links.Related, Links.Self, false)] - [InlineData(Links.Related, Links.Related, true)] - [InlineData(Links.Related, Links.Paging, false)] - [InlineData(Links.Related, Links.None, false)] - [InlineData(Links.Related, Links.NotConfigured, false)] - [InlineData(Links.Paging, Links.Self, false)] - [InlineData(Links.Paging, Links.Related, false)] - [InlineData(Links.Paging, Links.Paging, true)] - [InlineData(Links.Paging, Links.None, false)] - [InlineData(Links.Paging, Links.NotConfigured, false)] - public void LinkHasFlag_BaseLinkAndCheckLink_ExpectedResult(Links baseLink, Links checkLink, bool equal) + [InlineData(LinkTypes.All, LinkTypes.Self, true)] + [InlineData(LinkTypes.All, LinkTypes.Related, true)] + [InlineData(LinkTypes.All, LinkTypes.Paging, true)] + [InlineData(LinkTypes.None, LinkTypes.Self, false)] + [InlineData(LinkTypes.None, LinkTypes.Related, false)] + [InlineData(LinkTypes.None, LinkTypes.Paging, false)] + [InlineData(LinkTypes.NotConfigured, LinkTypes.Self, false)] + [InlineData(LinkTypes.NotConfigured, LinkTypes.Related, false)] + [InlineData(LinkTypes.NotConfigured, LinkTypes.Paging, false)] + [InlineData(LinkTypes.Self, LinkTypes.Self, true)] + [InlineData(LinkTypes.Self, LinkTypes.Related, false)] + [InlineData(LinkTypes.Self, LinkTypes.Paging, false)] + [InlineData(LinkTypes.Self, LinkTypes.None, false)] + [InlineData(LinkTypes.Self, LinkTypes.NotConfigured, false)] + [InlineData(LinkTypes.Related, LinkTypes.Self, false)] + [InlineData(LinkTypes.Related, LinkTypes.Related, true)] + [InlineData(LinkTypes.Related, LinkTypes.Paging, false)] + [InlineData(LinkTypes.Related, LinkTypes.None, false)] + [InlineData(LinkTypes.Related, LinkTypes.NotConfigured, false)] + [InlineData(LinkTypes.Paging, LinkTypes.Self, false)] + [InlineData(LinkTypes.Paging, LinkTypes.Related, false)] + [InlineData(LinkTypes.Paging, LinkTypes.Paging, true)] + [InlineData(LinkTypes.Paging, LinkTypes.None, false)] + [InlineData(LinkTypes.Paging, LinkTypes.NotConfigured, false)] + public void LinkHasFlag_BaseLinkAndCheckLink_ExpectedResult(LinkTypes baseLink, LinkTypes checkLink, bool equal) { Assert.Equal(equal, baseLink.HasFlag(checkLink)); } diff --git a/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs b/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs index 1015a4f889..10d71866b9 100644 --- a/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs +++ b/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs @@ -1,18 +1,18 @@ using System.Net; using System.Net.Http; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; -using Moq; -using Xunit; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; namespace UnitTests { @@ -26,14 +26,14 @@ public sealed class Resource : Identifiable public sealed class ResourceController : BaseJsonApiController { public ResourceController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) + : base(options, loggerFactory, resourceService) { } public ResourceController( - IJsonApiOptions jsonApiOptions, + IJsonApiOptions options, ILoggerFactory loggerFactory, IGetAllService getAll = null, IGetByIdService getById = null, @@ -43,7 +43,7 @@ public ResourceController( IUpdateService update = null, IUpdateRelationshipService updateRelationships = null, IDeleteService delete = null) - : base(jsonApiOptions, loggerFactory, getAll, getById, getSecondary, getRelationship, create, + : base(options, loggerFactory, getAll, getById, getSecondary, getRelationship, create, update, updateRelationships, delete) { } } diff --git a/test/UnitTests/Controllers/CoreJsonApiControllerTests.cs b/test/UnitTests/Controllers/CoreJsonApiControllerTests.cs index 02fd752344..215ac7ab8c 100644 --- a/test/UnitTests/Controllers/CoreJsonApiControllerTests.cs +++ b/test/UnitTests/Controllers/CoreJsonApiControllerTests.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Net; using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Mvc; using Xunit; diff --git a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index ff5d77e6eb..c8dd510bf6 100644 --- a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -1,25 +1,21 @@ -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Formatters; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Generics; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Xunit; -using Microsoft.EntityFrameworkCore; -using JsonApiDotNetCore.Models; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.RequestServices; -using JsonApiDotNetCore.RequestServices.Contracts; -using JsonApiDotNetCore.Serialization.Server.Builders; -using JsonApiDotNetCore.Serialization.Server; -using Microsoft.AspNetCore.Authentication; namespace UnitTests.Extensions { @@ -38,15 +34,15 @@ public void AddJsonApiInternals_Adds_All_Required_Services() // Act // this is required because the DbContextResolver requires access to the current HttpContext // to get the request scoped DbContext instance - services.AddScoped(); + services.AddScoped(); var provider = services.BuildServiceProvider(); // Assert - var currentRequest = provider.GetService() as CurrentRequest; - Assert.NotNull(currentRequest); + var request = provider.GetService() as JsonApiRequest; + Assert.NotNull(request); var resourceGraph = provider.GetService(); Assert.NotNull(resourceGraph); - currentRequest.PrimaryResource = resourceGraph.GetResourceContext(); + request.PrimaryResource = resourceGraph.GetResourceContext(); Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService(typeof(IResourceRepository))); @@ -74,7 +70,7 @@ public void RegisterResource_DeviatingDbContextPropertyName_RegistersCorrectly() // Act // this is required because the DbContextResolver requires access to the current HttpContext // to get the request scoped DbContext instance - services.AddScoped(); + services.AddScoped(); var provider = services.BuildServiceProvider(); var graph = provider.GetService(); var resourceContext = graph.GetResourceContext(); @@ -136,7 +132,7 @@ public void AddResourceService_Throws_If_Type_Does_Not_Implement_Any_Interfaces( var services = new ServiceCollection(); // Act, assert - Assert.Throws(() => services.AddResourceService()); + Assert.Throws(() => services.AddResourceService()); } [Fact] @@ -147,7 +143,7 @@ public void AddJsonApi_With_Context_Uses_Resource_Type_Name_If_NoOtherSpecified( services.AddLogging(); services.AddDbContext(options => options.UseInMemoryDatabase(Guid.NewGuid().ToString())); - services.AddScoped(); + services.AddScoped(); // Act services.AddJsonApi(); diff --git a/test/UnitTests/Extensions/TypeExtensions_Tests.cs b/test/UnitTests/Extensions/TypeExtensions_Tests.cs deleted file mode 100644 index 7bdba9ee32..0000000000 --- a/test/UnitTests/Extensions/TypeExtensions_Tests.cs +++ /dev/null @@ -1,55 +0,0 @@ -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using Xunit; - -namespace UnitTests.Extensions -{ - public sealed class TypeExtensions_Tests - { - [Fact] - public void New_Creates_An_Instance_If_T_Implements_Interface() - { - // Arrange - var type = typeof(Model); - - // Act - var instance = (IIdentifiable)TypeHelper.CreateInstance(type); - - // Assert - Assert.NotNull(instance); - Assert.IsType(instance); - } - - [Fact] - public void Implements_Returns_True_If_Type_Implements_Interface() - { - // Arrange - var type = typeof(Model); - - // Act - var result = type.IsOrImplementsInterface(typeof(IIdentifiable)); - - // Assert - Assert.True(result); - } - - [Fact] - public void Implements_Returns_False_If_Type_DoesNot_Implement_Interface() - { - // Arrange - var type = typeof(string); - - // Act - var result = type.IsOrImplementsInterface(typeof(IIdentifiable)); - - // Assert - Assert.False(result); - } - - private sealed class Model : IIdentifiable - { - public string StringId { get; set; } - } - } -} diff --git a/test/UnitTests/Graph/IdentifiableTypeCacheTests.cs b/test/UnitTests/Graph/IdentifiableTypeCacheTests.cs index a0b4220f75..352b3e4f4e 100644 --- a/test/UnitTests/Graph/IdentifiableTypeCacheTests.cs +++ b/test/UnitTests/Graph/IdentifiableTypeCacheTests.cs @@ -1,5 +1,5 @@ -using JsonApiDotNetCore.Graph; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; using UnitTests.Internal; using Xunit; diff --git a/test/UnitTests/Graph/TypeLocator_Tests.cs b/test/UnitTests/Graph/TypeLocator_Tests.cs index e15b037c2b..03d17f8dcb 100644 --- a/test/UnitTests/Graph/TypeLocator_Tests.cs +++ b/test/UnitTests/Graph/TypeLocator_Tests.cs @@ -1,6 +1,6 @@ using System; -using JsonApiDotNetCore.Graph; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; using Xunit; namespace UnitTests.Internal diff --git a/test/UnitTests/Internal/ErrorDocumentTests.cs b/test/UnitTests/Internal/ErrorDocumentTests.cs index a8b2946ca2..4a7dd40a24 100644 --- a/test/UnitTests/Internal/ErrorDocumentTests.cs +++ b/test/UnitTests/Internal/ErrorDocumentTests.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Net; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Serialization.Objects; using Xunit; namespace UnitTests.Internal diff --git a/test/UnitTests/Internal/RequestScopedServiceProviderTests.cs b/test/UnitTests/Internal/RequestScopedServiceProviderTests.cs index 2b5fbe9119..6fe5da75ab 100644 --- a/test/UnitTests/Internal/RequestScopedServiceProviderTests.cs +++ b/test/UnitTests/Internal/RequestScopedServiceProviderTests.cs @@ -1,6 +1,6 @@ using System; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Http; using Xunit; @@ -13,17 +13,17 @@ public sealed class RequestScopedServiceProviderTests public void When_http_context_is_unavailable_it_must_fail() { // Arrange + var serviceType = typeof(IIdentifiable); + var provider = new RequestScopedServiceProvider(new HttpContextAccessor()); // Act - Action action = () => provider.GetService(typeof(IIdentifiable)); + Action action = () => provider.GetService(serviceType); // Assert var exception = Assert.Throws(action); - Assert.StartsWith("Cannot resolve scoped service " + - "'JsonApiDotNetCore.Models.IIdentifiable`1[[JsonApiDotNetCoreExample.Models.Tag, JsonApiDotNetCoreExample, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]' " + - "outside the context of an HTTP request.", exception.Message); + Assert.StartsWith("Cannot resolve scoped service " + $"'{serviceType.FullName}' outside the context of an HTTP request.", exception.Message); } } } diff --git a/test/UnitTests/Internal/ResourceGraphBuilderTests.cs b/test/UnitTests/Internal/ResourceGraphBuilderTests.cs index f76a0f8bf7..c5350640db 100644 --- a/test/UnitTests/Internal/ResourceGraphBuilderTests.cs +++ b/test/UnitTests/Internal/ResourceGraphBuilderTests.cs @@ -1,11 +1,9 @@ -using JsonApiDotNetCore.Builders; +using Castle.DynamicProxy; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Resources; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Castle.DynamicProxy; using Xunit; namespace UnitTests.Internal @@ -19,7 +17,7 @@ public void AddDbContext_Does_Not_Throw_If_Context_Contains_Members_That_Do_Not_ var resourceGraphBuilder = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance); // Act - resourceGraphBuilder.AddResource(typeof(TestContext)); + resourceGraphBuilder.Add(typeof(TestContext)); var resourceGraph = (ResourceGraph)resourceGraphBuilder.Build(); // Assert @@ -32,7 +30,7 @@ public void Adding_DbContext_Members_That_Do_Not_Implement_IIdentifiable_Logs_Wa // Arrange var loggerFactory = new FakeLoggerFactory(); var resourceGraphBuilder = new ResourceGraphBuilder(new JsonApiOptions(), loggerFactory); - resourceGraphBuilder.AddResource(typeof(TestContext)); + resourceGraphBuilder.Add(typeof(TestContext)); // Act resourceGraphBuilder.Build(); @@ -48,7 +46,7 @@ public void GetResourceContext_Yields_Right_Type_For_LazyLoadingProxy() { // Arrange var resourceGraphBuilder = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance); - resourceGraphBuilder.AddResource(); + resourceGraphBuilder.Add(); var resourceGraph = (ResourceGraph)resourceGraphBuilder.Build(); var proxyGenerator = new ProxyGenerator(); @@ -65,7 +63,7 @@ public void GetResourceContext_Yields_Right_Type_For_Identifiable() { // Arrange var resourceGraphBuilder = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance); - resourceGraphBuilder.AddResource(); + resourceGraphBuilder.Add(); var resourceGraph = (ResourceGraph)resourceGraphBuilder.Build(); // Act diff --git a/test/UnitTests/Internal/TypeHelper_Tests.cs b/test/UnitTests/Internal/TypeHelper_Tests.cs index 89c926aaab..7185641091 100644 --- a/test/UnitTests/Internal/TypeHelper_Tests.cs +++ b/test/UnitTests/Internal/TypeHelper_Tests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; -using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore; +using JsonApiDotNetCore.Resources; using Xunit; namespace UnitTests.Internal @@ -131,6 +132,46 @@ public void Bad_TimeSpanString_Throws() Assert.Throws(() => TypeHelper.ConvertType(formattedString, typeof(TimeSpan))); } + [Fact] + public void New_Creates_An_Instance_If_T_Implements_Interface() + { + // Arrange + var type = typeof(Model); + + // Act + var instance = (IIdentifiable)TypeHelper.CreateInstance(type); + + // Assert + Assert.NotNull(instance); + Assert.IsType(instance); + } + + [Fact] + public void Implements_Returns_True_If_Type_Implements_Interface() + { + // Arrange + var type = typeof(Model); + + // Act + var result = TypeHelper.IsOrImplementsInterface(type, typeof(IIdentifiable)); + + // Assert + Assert.True(result); + } + + [Fact] + public void Implements_Returns_False_If_Type_DoesNot_Implement_Interface() + { + // Arrange + var type = typeof(string); + + // Act + var result = TypeHelper.IsOrImplementsInterface(type, typeof(IIdentifiable)); + + // Assert + Assert.False(result); + } + private enum TestEnum { Test = 1 @@ -146,5 +187,10 @@ private class BaseType : IType private interface IType { } + + private sealed class Model : IIdentifiable + { + public string StringId { get; set; } + } } } diff --git a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs index af2e9ecaf2..75130f1b3f 100644 --- a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs +++ b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs @@ -1,16 +1,13 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources.Annotations; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Moq; -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.RequestServices; using Xunit; namespace UnitTests.Middleware @@ -23,28 +20,28 @@ public async Task ParseUrlBase_ObfuscatedIdClass_ShouldSetIdCorrectly() // Arrange var id = "ABC123ABC"; var configuration = GetConfiguration($"/obfuscatedIdModel/{id}", action: "GetAsync", id: id); - var currentRequest = configuration.CurrentRequest; + var request = configuration.Request; // Act await RunMiddlewareTask(configuration); // Assert - Assert.Equal(id, currentRequest.PrimaryId); + Assert.Equal(id, request.PrimaryId); } [Fact] - public async Task ParseUrlBase_UrlHasPrimaryIdSet_ShouldSetCurrentRequestWithSaidId() + public async Task ParseUrlBase_UrlHasPrimaryIdSet_ShouldSetupRequestWithSameId() { // Arrange var id = "123"; var configuration = GetConfiguration($"/users/{id}", id: id); - var currentRequest = configuration.CurrentRequest; + var request = configuration.Request; // Act await RunMiddlewareTask(configuration); // Assert - Assert.Equal(id, currentRequest.PrimaryId); + Assert.Equal(id, request.PrimaryId); } [Fact] @@ -52,13 +49,13 @@ public async Task ParseUrlBase_UrlHasNoPrimaryIdSet_ShouldHaveBaseIdSetToNull() { // Arrange var configuration = GetConfiguration("/users"); - var currentRequest = configuration.CurrentRequest; + var request = configuration.Request; // Act await RunMiddlewareTask(configuration); // Assert - Assert.Null(currentRequest.PrimaryId); + Assert.Null(request.PrimaryId); } [Fact] @@ -77,7 +74,7 @@ private sealed class InvokeConfiguration public HttpContext HttpContext; public Mock ControllerResourceMapping; public Mock Options; - public CurrentRequest CurrentRequest; + public JsonApiRequest Request; public Mock ResourceGraph; } private Task RunMiddlewareTask(InvokeConfiguration holder) @@ -85,9 +82,9 @@ private Task RunMiddlewareTask(InvokeConfiguration holder) var controllerResourceMapping = holder.ControllerResourceMapping.Object; var context = holder.HttpContext; var options = holder.Options.Object; - var currentRequest = holder.CurrentRequest; + var request = holder.Request; var resourceGraph = holder.ResourceGraph.Object; - return holder.MiddleWare.Invoke(context, controllerResourceMapping, options, currentRequest, resourceGraph); + return holder.MiddleWare.Invoke(context, controllerResourceMapping, options, request, resourceGraph); } private InvokeConfiguration GetConfiguration(string path, string resourceName = "users", string action = "", string id =null, Type relType = null) { @@ -101,12 +98,14 @@ private InvokeConfiguration GetConfiguration(string path, string resourceName = }); var forcedNamespace = "api/v1"; var mockMapping = new Mock(); + mockMapping.Setup(x => x.GetAssociatedResource(It.IsAny())).Returns(typeof(string)); + Mock mockOptions = CreateMockOptions(forcedNamespace); var mockGraph = CreateMockResourceGraph(resourceName, includeRelationship: relType != null); - var currentRequest = new CurrentRequest(); + var request = new JsonApiRequest(); if (relType != null) { - currentRequest.Relationship = new HasManyAttribute + request.Relationship = new HasManyAttribute { RightType = relType }; @@ -117,7 +116,7 @@ private InvokeConfiguration GetConfiguration(string path, string resourceName = MiddleWare = middleware, ControllerResourceMapping = mockMapping, Options = mockOptions, - CurrentRequest = currentRequest, + Request = request, HttpContext = context, ResourceGraph = mockGraph }; diff --git a/test/UnitTests/Models/AttributesEqualsTests.cs b/test/UnitTests/Models/AttributesEqualsTests.cs index 35c9b3c0c8..74879accbb 100644 --- a/test/UnitTests/Models/AttributesEqualsTests.cs +++ b/test/UnitTests/Models/AttributesEqualsTests.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources.Annotations; using Xunit; namespace UnitTests.Models @@ -8,8 +8,8 @@ public sealed class AttributesEqualsTests [Fact] public void HasManyAttribute_Equals_Returns_True_When_Same_Name() { - var a = new HasManyAttribute("test"); - var b = new HasManyAttribute("test"); + var a = new HasManyAttribute {PublicName = "test"}; + var b = new HasManyAttribute {PublicName = "test"}; Assert.Equal(a, b); } @@ -17,8 +17,8 @@ public void HasManyAttribute_Equals_Returns_True_When_Same_Name() [Fact] public void HasManyAttribute_Equals_Returns_False_When_Different_Name() { - var a = new HasManyAttribute("test"); - var b = new HasManyAttribute("test2"); + var a = new HasManyAttribute {PublicName = "test"}; + var b = new HasManyAttribute {PublicName = "test2"}; Assert.NotEqual(a, b); } @@ -26,8 +26,8 @@ public void HasManyAttribute_Equals_Returns_False_When_Different_Name() [Fact] public void HasOneAttribute_Equals_Returns_True_When_Same_Name() { - var a = new HasOneAttribute("test"); - var b = new HasOneAttribute("test"); + var a = new HasOneAttribute {PublicName = "test"}; + var b = new HasOneAttribute {PublicName = "test"}; Assert.Equal(a, b); } @@ -35,8 +35,8 @@ public void HasOneAttribute_Equals_Returns_True_When_Same_Name() [Fact] public void HasOneAttribute_Equals_Returns_False_When_Different_Name() { - var a = new HasOneAttribute("test"); - var b = new HasOneAttribute("test2"); + var a = new HasOneAttribute {PublicName = "test"}; + var b = new HasOneAttribute {PublicName = "test2"}; Assert.NotEqual(a, b); } @@ -44,8 +44,8 @@ public void HasOneAttribute_Equals_Returns_False_When_Different_Name() [Fact] public void AttrAttribute_Equals_Returns_True_When_Same_Name() { - var a = new AttrAttribute("test"); - var b = new AttrAttribute("test"); + var a = new AttrAttribute {PublicName = "test"}; + var b = new AttrAttribute {PublicName = "test"}; Assert.Equal(a, b); } @@ -53,8 +53,8 @@ public void AttrAttribute_Equals_Returns_True_When_Same_Name() [Fact] public void AttrAttribute_Equals_Returns_False_When_Different_Name() { - var a = new AttrAttribute("test"); - var b = new AttrAttribute("test2"); + var a = new AttrAttribute {PublicName = "test"}; + var b = new AttrAttribute {PublicName = "test2"}; Assert.NotEqual(a, b); } @@ -62,8 +62,8 @@ public void AttrAttribute_Equals_Returns_False_When_Different_Name() [Fact] public void HasManyAttribute_Does_Not_Equal_HasOneAttribute_With_Same_Name() { - RelationshipAttribute a = new HasManyAttribute("test"); - RelationshipAttribute b = new HasOneAttribute("test"); + RelationshipAttribute a = new HasManyAttribute {PublicName = "test"}; + RelationshipAttribute b = new HasOneAttribute {PublicName = "test"}; Assert.NotEqual(a, b); Assert.NotEqual(b, a); diff --git a/test/UnitTests/Models/IdentifiableTests.cs b/test/UnitTests/Models/IdentifiableTests.cs index 971a936e39..cf90129eac 100644 --- a/test/UnitTests/Models/IdentifiableTests.cs +++ b/test/UnitTests/Models/IdentifiableTests.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Resources; using Xunit; namespace UnitTests.Models @@ -20,15 +20,15 @@ public void Setting_StringId_To_Null_Sets_Id_As_Default() } [Fact] - public void GetStringId_Returns_EmptyString_If_Object_Is_Null() + public void GetStringId_Returns_Null_If_Object_Is_Default() { var resource = new IntId(); - var stringId = resource.ExposedGetStringId(null); - Assert.Equal(string.Empty, stringId); + var stringId = resource.ExposedGetStringId(default); + Assert.Null(stringId); } private sealed class IntId : Identifiable { - public string ExposedGetStringId(object value) => GetStringId(value); + public string ExposedGetStringId(int value) => GetStringId(value); } } } diff --git a/test/UnitTests/Models/LinkTests.cs b/test/UnitTests/Models/LinkTests.cs index 71ce2fef97..cda2699d39 100644 --- a/test/UnitTests/Models/LinkTests.cs +++ b/test/UnitTests/Models/LinkTests.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Resources.Annotations; using Xunit; namespace UnitTests.Models @@ -9,70 +9,70 @@ public sealed class LinkTests public void All_Contains_All_Flags_Except_None() { // Arrange - var e = Links.All; + var e = LinkTypes.All; // Assert - Assert.True(e.HasFlag(Links.Self)); - Assert.True(e.HasFlag(Links.Paging)); - Assert.True(e.HasFlag(Links.Related)); - Assert.True(e.HasFlag(Links.All)); - Assert.False(e.HasFlag(Links.None)); + Assert.True(e.HasFlag(LinkTypes.Self)); + Assert.True(e.HasFlag(LinkTypes.Paging)); + Assert.True(e.HasFlag(LinkTypes.Related)); + Assert.True(e.HasFlag(LinkTypes.All)); + Assert.False(e.HasFlag(LinkTypes.None)); } [Fact] public void None_Contains_Only_None() { // Arrange - var e = Links.None; + var e = LinkTypes.None; // Assert - Assert.False(e.HasFlag(Links.Self)); - Assert.False(e.HasFlag(Links.Paging)); - Assert.False(e.HasFlag(Links.Related)); - Assert.False(e.HasFlag(Links.All)); - Assert.True(e.HasFlag(Links.None)); + Assert.False(e.HasFlag(LinkTypes.Self)); + Assert.False(e.HasFlag(LinkTypes.Paging)); + Assert.False(e.HasFlag(LinkTypes.Related)); + Assert.False(e.HasFlag(LinkTypes.All)); + Assert.True(e.HasFlag(LinkTypes.None)); } [Fact] public void Self() { // Arrange - var e = Links.Self; + var e = LinkTypes.Self; // Assert - Assert.True(e.HasFlag(Links.Self)); - Assert.False(e.HasFlag(Links.Paging)); - Assert.False(e.HasFlag(Links.Related)); - Assert.False(e.HasFlag(Links.All)); - Assert.False(e.HasFlag(Links.None)); + Assert.True(e.HasFlag(LinkTypes.Self)); + Assert.False(e.HasFlag(LinkTypes.Paging)); + Assert.False(e.HasFlag(LinkTypes.Related)); + Assert.False(e.HasFlag(LinkTypes.All)); + Assert.False(e.HasFlag(LinkTypes.None)); } [Fact] public void Paging() { // Arrange - var e = Links.Paging; + var e = LinkTypes.Paging; // Assert - Assert.False(e.HasFlag(Links.Self)); - Assert.True(e.HasFlag(Links.Paging)); - Assert.False(e.HasFlag(Links.Related)); - Assert.False(e.HasFlag(Links.All)); - Assert.False(e.HasFlag(Links.None)); + Assert.False(e.HasFlag(LinkTypes.Self)); + Assert.True(e.HasFlag(LinkTypes.Paging)); + Assert.False(e.HasFlag(LinkTypes.Related)); + Assert.False(e.HasFlag(LinkTypes.All)); + Assert.False(e.HasFlag(LinkTypes.None)); } [Fact] public void Related() { // Arrange - var e = Links.Related; + var e = LinkTypes.Related; // Assert - Assert.False(e.HasFlag(Links.Self)); - Assert.False(e.HasFlag(Links.Paging)); - Assert.True(e.HasFlag(Links.Related)); - Assert.False(e.HasFlag(Links.All)); - Assert.False(e.HasFlag(Links.None)); + Assert.False(e.HasFlag(LinkTypes.Self)); + Assert.False(e.HasFlag(LinkTypes.Paging)); + Assert.True(e.HasFlag(LinkTypes.Related)); + Assert.False(e.HasFlag(LinkTypes.All)); + Assert.False(e.HasFlag(LinkTypes.None)); } } } diff --git a/test/UnitTests/Models/RelationshipDataTests.cs b/test/UnitTests/Models/RelationshipDataTests.cs index 8eb271ff52..4fc3d2f98c 100644 --- a/test/UnitTests/Models/RelationshipDataTests.cs +++ b/test/UnitTests/Models/RelationshipDataTests.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization.Objects; using Newtonsoft.Json.Linq; using Xunit; diff --git a/test/UnitTests/Models/ResourceConstructionExpressionTests.cs b/test/UnitTests/Models/ResourceConstructionExpressionTests.cs index 8c93b34999..242ee0feef 100644 --- a/test/UnitTests/Models/ResourceConstructionExpressionTests.cs +++ b/test/UnitTests/Models/ResourceConstructionExpressionTests.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel.Design; using System.Linq.Expressions; -using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Resources; using JsonApiDotNetCoreExample.Data; using Microsoft.AspNetCore.Authentication; using Microsoft.EntityFrameworkCore; diff --git a/test/UnitTests/Models/ResourceConstructionTests.cs b/test/UnitTests/Models/ResourceConstructionTests.cs index dd2f59eb81..fb0cacc5b5 100644 --- a/test/UnitTests/Models/ResourceConstructionTests.cs +++ b/test/UnitTests/Models/ResourceConstructionTests.cs @@ -1,11 +1,8 @@ using System; using System.ComponentModel.Design; -using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Serialization.Server; using JsonApiDotNetCoreExample.Data; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; @@ -31,7 +28,7 @@ public void When_resource_has_default_constructor_it_must_succeed() { // Arrange var graph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance) - .AddResource() + .Add() .Build(); var serializer = new RequestDeserializer(graph, new ResourceFactory(new ServiceContainer()), new TargetedFields(), _mockHttpContextAccessor.Object); @@ -60,7 +57,7 @@ public void When_resource_has_default_constructor_that_throws_it_must_fail() { // Arrange var graph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance) - .AddResource() + .Add() .Build(); var serializer = new RequestDeserializer(graph, new ResourceFactory(new ServiceContainer()), new TargetedFields(), _mockHttpContextAccessor.Object); @@ -91,7 +88,7 @@ public void When_resource_has_constructor_with_injectable_parameter_it_must_succ { // Arrange var graph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance) - .AddResource() + .Add() .Build(); var appDbContext = new AppDbContext(new DbContextOptionsBuilder().Options, new FrozenSystemClock()); @@ -126,7 +123,7 @@ public void When_resource_has_constructor_with_string_parameter_it_must_fail() { // Arrange var graph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance) - .AddResource() + .Add() .Build(); var serializer = new RequestDeserializer(graph, new ResourceFactory(new ServiceContainer()), new TargetedFields(), _mockHttpContextAccessor.Object); diff --git a/test/UnitTests/QueryStringParameters/ParseTestsBase.cs b/test/UnitTests/QueryStringParameters/BaseParseTests.cs similarity index 51% rename from test/UnitTests/QueryStringParameters/ParseTestsBase.cs rename to test/UnitTests/QueryStringParameters/BaseParseTests.cs index 4717c2c11f..3952d9af61 100644 --- a/test/UnitTests/QueryStringParameters/ParseTestsBase.cs +++ b/test/UnitTests/QueryStringParameters/BaseParseTests.cs @@ -1,33 +1,31 @@ -using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.RequestServices; +using JsonApiDotNetCore.Middleware; using JsonApiDotNetCoreExample.Models; using Microsoft.Extensions.Logging.Abstractions; namespace UnitTests.QueryStringParameters { - public abstract class ParseTestsBase + public abstract class BaseParseTests { protected JsonApiOptions Options { get; } protected IResourceGraph ResourceGraph { get; } - protected CurrentRequest CurrentRequest { get; } + protected JsonApiRequest Request { get; } - protected ParseTestsBase() + protected BaseParseTests() { Options = new JsonApiOptions(); ResourceGraph = new ResourceGraphBuilder(Options, NullLoggerFactory.Instance) - .AddResource() - .AddResource
() - .AddResource() - .AddResource
() - .AddResource() - .AddResource() - .AddResource() + .Add() + .Add
() + .Add() + .Add
() + .Add() + .Add() + .Add() .Build(); - CurrentRequest = new CurrentRequest + Request = new JsonApiRequest { PrimaryResource = ResourceGraph.GetResourceContext(), IsCollection = true diff --git a/test/UnitTests/QueryStringParameters/DefaultsParseTests.cs b/test/UnitTests/QueryStringParameters/DefaultsParseTests.cs index ea34478458..90459eaa36 100644 --- a/test/UnitTests/QueryStringParameters/DefaultsParseTests.cs +++ b/test/UnitTests/QueryStringParameters/DefaultsParseTests.cs @@ -2,10 +2,10 @@ using System.Net; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Internal.QueryStrings; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.QueryStrings; +using JsonApiDotNetCore.QueryStrings.Internal; using Newtonsoft.Json; using Xunit; @@ -53,7 +53,7 @@ public void Reader_Is_Enabled(StandardQueryStringParameters parametersDisabled, var reader = new DefaultsQueryStringParameterReader(options); // Act - var isEnabled = reader.IsEnabled(new DisableQueryAttribute(parametersDisabled)); + var isEnabled = reader.IsEnabled(new DisableQueryStringAttribute(parametersDisabled)); // Assert isEnabled.Should().Be(allowOverride && expectIsEnabled); @@ -118,7 +118,7 @@ public void Reader_Outcome(string queryStringParameterValue, DefaultValueHandlin var reader = new DefaultsQueryStringParameterReader(options); // Act - if (reader.IsEnabled(DisableQueryAttribute.Empty)) + if (reader.IsEnabled(DisableQueryStringAttribute.Empty)) { reader.Read("defaults", queryStringParameterValue); } diff --git a/test/UnitTests/QueryStringParameters/FilterParseTests.cs b/test/UnitTests/QueryStringParameters/FilterParseTests.cs index 4ababfd6bd..dc8b74411b 100644 --- a/test/UnitTests/QueryStringParameters/FilterParseTests.cs +++ b/test/UnitTests/QueryStringParameters/FilterParseTests.cs @@ -3,15 +3,16 @@ using System.Linq; using System.Net; using FluentAssertions; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.QueryStrings; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.QueryStrings; +using JsonApiDotNetCore.QueryStrings.Internal; +using JsonApiDotNetCore.Resources; using Xunit; namespace UnitTests.QueryStringParameters { - public sealed class FilterParseTests : ParseTestsBase + public sealed class FilterParseTests : BaseParseTests { private readonly FilterQueryStringParameterReader _reader; @@ -20,7 +21,7 @@ public FilterParseTests() Options.EnableLegacyFilterNotation = false; var resourceFactory = new ResourceFactory(new ServiceContainer()); - _reader = new FilterQueryStringParameterReader(CurrentRequest, ResourceGraph, resourceFactory, Options); + _reader = new FilterQueryStringParameterReader(Request, ResourceGraph, resourceFactory, Options); } [Theory] @@ -46,7 +47,7 @@ public void Reader_Supports_Parameter_Name(string parameterName, bool expectCanP public void Reader_Is_Enabled(StandardQueryStringParameters parametersDisabled, bool expectIsEnabled) { // Act - var isEnabled = _reader.IsEnabled(new DisableQueryAttribute(parametersDisabled)); + var isEnabled = _reader.IsEnabled(new DisableQueryStringAttribute(parametersDisabled)); // Assert isEnabled.Should().Be(expectIsEnabled); diff --git a/test/UnitTests/QueryStringParameters/IncludeParseTests.cs b/test/UnitTests/QueryStringParameters/IncludeParseTests.cs index fba42b0b3a..a52dc5ddc1 100644 --- a/test/UnitTests/QueryStringParameters/IncludeParseTests.cs +++ b/test/UnitTests/QueryStringParameters/IncludeParseTests.cs @@ -3,20 +3,21 @@ using System.Net; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Internal.QueryStrings; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.QueryStrings; +using JsonApiDotNetCore.QueryStrings.Internal; using Xunit; namespace UnitTests.QueryStringParameters { - public sealed class IncludeParseTests : ParseTestsBase + public sealed class IncludeParseTests : BaseParseTests { private readonly IncludeQueryStringParameterReader _reader; public IncludeParseTests() { - _reader = new IncludeQueryStringParameterReader(CurrentRequest, ResourceGraph, new JsonApiOptions()); + _reader = new IncludeQueryStringParameterReader(Request, ResourceGraph, new JsonApiOptions()); } [Theory] @@ -40,7 +41,7 @@ public void Reader_Supports_Parameter_Name(string parameterName, bool expectCanP public void Reader_Is_Enabled(StandardQueryStringParameters parametersDisabled, bool expectIsEnabled) { // Act - var isEnabled = _reader.IsEnabled(new DisableQueryAttribute(parametersDisabled)); + var isEnabled = _reader.IsEnabled(new DisableQueryStringAttribute(parametersDisabled)); // Assert isEnabled.Should().Be(expectIsEnabled); diff --git a/test/UnitTests/QueryStringParameters/LegacyFilterParseTests.cs b/test/UnitTests/QueryStringParameters/LegacyFilterParseTests.cs index 523b47a78b..b12a389a95 100644 --- a/test/UnitTests/QueryStringParameters/LegacyFilterParseTests.cs +++ b/test/UnitTests/QueryStringParameters/LegacyFilterParseTests.cs @@ -3,15 +3,15 @@ using System.Linq; using System.Net; using FluentAssertions; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.QueryStrings; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.QueryStrings.Internal; +using JsonApiDotNetCore.Resources; using JsonApiDotNetCoreExample.Models; using Xunit; namespace UnitTests.QueryStringParameters { - public sealed class LegacyFilterParseTests : ParseTestsBase + public sealed class LegacyFilterParseTests : BaseParseTests { private readonly FilterQueryStringParameterReader _reader; @@ -19,10 +19,10 @@ public LegacyFilterParseTests() { Options.EnableLegacyFilterNotation = true; - CurrentRequest.PrimaryResource = ResourceGraph.GetResourceContext
(); + Request.PrimaryResource = ResourceGraph.GetResourceContext
(); var resourceFactory = new ResourceFactory(new ServiceContainer()); - _reader = new FilterQueryStringParameterReader(CurrentRequest, ResourceGraph, resourceFactory, Options); + _reader = new FilterQueryStringParameterReader(Request, ResourceGraph, resourceFactory, Options); } [Theory] diff --git a/test/UnitTests/QueryStringParameters/NullsParseTests.cs b/test/UnitTests/QueryStringParameters/NullsParseTests.cs index c8cfff50b8..5d754cb17b 100644 --- a/test/UnitTests/QueryStringParameters/NullsParseTests.cs +++ b/test/UnitTests/QueryStringParameters/NullsParseTests.cs @@ -2,10 +2,10 @@ using System.Net; using FluentAssertions; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Internal.QueryStrings; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.QueryStrings; +using JsonApiDotNetCore.QueryStrings.Internal; using Newtonsoft.Json; using Xunit; @@ -53,7 +53,7 @@ public void Reader_Is_Enabled(StandardQueryStringParameters parametersDisabled, var reader = new NullsQueryStringParameterReader(options); // Act - var isEnabled = reader.IsEnabled(new DisableQueryAttribute(parametersDisabled)); + var isEnabled = reader.IsEnabled(new DisableQueryStringAttribute(parametersDisabled)); // Assert isEnabled.Should().Be(allowOverride && expectIsEnabled); @@ -118,7 +118,7 @@ public void Reader_Outcome(string queryStringParameterValue, NullValueHandling o var reader = new NullsQueryStringParameterReader(options); // Act - if (reader.IsEnabled(DisableQueryAttribute.Empty)) + if (reader.IsEnabled(DisableQueryStringAttribute.Empty)) { reader.Read("nulls", queryStringParameterValue); } diff --git a/test/UnitTests/QueryStringParameters/PaginationParseTests.cs b/test/UnitTests/QueryStringParameters/PaginationParseTests.cs index 3705de7c5e..d62b379587 100644 --- a/test/UnitTests/QueryStringParameters/PaginationParseTests.cs +++ b/test/UnitTests/QueryStringParameters/PaginationParseTests.cs @@ -2,23 +2,23 @@ using System.Linq; using System.Net; using FluentAssertions; -using JsonApiDotNetCore; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Internal.QueryStrings; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.QueryStrings; +using JsonApiDotNetCore.QueryStrings.Internal; using Xunit; namespace UnitTests.QueryStringParameters { - public sealed class PaginationParseTests : ParseTestsBase + public sealed class PaginationParseTests : BaseParseTests { private readonly IPaginationQueryStringParameterReader _reader; public PaginationParseTests() { Options.DefaultPageSize = new PageSize(25); - _reader = new PaginationQueryStringParameterReader(CurrentRequest, ResourceGraph, Options); + _reader = new PaginationQueryStringParameterReader(Request, ResourceGraph, Options); } [Theory] @@ -44,7 +44,7 @@ public void Reader_Supports_Parameter_Name(string parameterName, bool expectCanP public void Reader_Is_Enabled(StandardQueryStringParameters parametersDisabled, bool expectIsEnabled) { // Act - var isEnabled = _reader.IsEnabled(new DisableQueryAttribute(parametersDisabled)); + var isEnabled = _reader.IsEnabled(new DisableQueryStringAttribute(parametersDisabled)); // Assert isEnabled.Should().Be(expectIsEnabled); diff --git a/test/UnitTests/QueryStringParameters/SortParseTests.cs b/test/UnitTests/QueryStringParameters/SortParseTests.cs index fdae623781..c3b9c7ce09 100644 --- a/test/UnitTests/QueryStringParameters/SortParseTests.cs +++ b/test/UnitTests/QueryStringParameters/SortParseTests.cs @@ -2,20 +2,21 @@ using System.Linq; using System.Net; using FluentAssertions; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Internal.QueryStrings; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.QueryStrings; +using JsonApiDotNetCore.QueryStrings.Internal; using Xunit; namespace UnitTests.QueryStringParameters { - public sealed class SortParseTests : ParseTestsBase + public sealed class SortParseTests : BaseParseTests { private readonly SortQueryStringParameterReader _reader; public SortParseTests() { - _reader = new SortQueryStringParameterReader(CurrentRequest, ResourceGraph); + _reader = new SortQueryStringParameterReader(Request, ResourceGraph); } [Theory] @@ -42,7 +43,7 @@ public void Reader_Supports_Parameter_Name(string parameterName, bool expectCanP public void Reader_Is_Enabled(StandardQueryStringParameters parametersDisabled, bool expectIsEnabled) { // Act - var isEnabled = _reader.IsEnabled(new DisableQueryAttribute(parametersDisabled)); + var isEnabled = _reader.IsEnabled(new DisableQueryStringAttribute(parametersDisabled)); // Assert isEnabled.Should().Be(expectIsEnabled); diff --git a/test/UnitTests/QueryStringParameters/SparseFieldSetParseTests.cs b/test/UnitTests/QueryStringParameters/SparseFieldSetParseTests.cs index b6083150d2..82decd2696 100644 --- a/test/UnitTests/QueryStringParameters/SparseFieldSetParseTests.cs +++ b/test/UnitTests/QueryStringParameters/SparseFieldSetParseTests.cs @@ -2,20 +2,21 @@ using System.Linq; using System.Net; using FluentAssertions; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Internal.QueryStrings; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.QueryStrings; +using JsonApiDotNetCore.QueryStrings.Internal; using Xunit; namespace UnitTests.QueryStringParameters { - public sealed class SparseFieldSetParseTests : ParseTestsBase + public sealed class SparseFieldSetParseTests : BaseParseTests { private readonly SparseFieldSetQueryStringParameterReader _reader; public SparseFieldSetParseTests() { - _reader = new SparseFieldSetQueryStringParameterReader(CurrentRequest, ResourceGraph); + _reader = new SparseFieldSetQueryStringParameterReader(Request, ResourceGraph); } [Theory] @@ -42,7 +43,7 @@ public void Reader_Supports_Parameter_Name(string parameterName, bool expectCanP public void Reader_Is_Enabled(StandardQueryStringParameters parametersDisabled, bool expectIsEnabled) { // Act - var isEnabled = _reader.IsEnabled(new DisableQueryAttribute(parametersDisabled)); + var isEnabled = _reader.IsEnabled(new DisableQueryStringAttribute(parametersDisabled)); // Assert isEnabled.Should().Be(expectIsEnabled); diff --git a/test/UnitTests/ResourceHooks/DiscoveryTests.cs b/test/UnitTests/ResourceHooks/DiscoveryTests.cs index cf658034f2..e05da73ba0 100644 --- a/test/UnitTests/ResourceHooks/DiscoveryTests.cs +++ b/test/UnitTests/ResourceHooks/DiscoveryTests.cs @@ -1,14 +1,13 @@ -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Hooks; -using System.Collections.Generic; -using Xunit; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; using System; +using System.Collections.Generic; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Hooks.Internal.Discovery; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Resources; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; +using Xunit; namespace UnitTests.ResourceHooks { @@ -17,7 +16,7 @@ public sealed class DiscoveryTests public class Dummy : Identifiable { } public sealed class DummyResourceDefinition : ResourceDefinition { - public DummyResourceDefinition() : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).AddResource().Build()) { } + public DummyResourceDefinition() : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).Add().Build()) { } public override IEnumerable BeforeDelete(IResourceHashSet affected, ResourcePipeline pipeline) { return affected; } public override void AfterDelete(HashSet resources, ResourcePipeline pipeline, bool succeeded) { } @@ -50,7 +49,7 @@ public override void AfterDelete(HashSet resources, ResourcePipeline pipeline public sealed class AnotherDummyResourceDefinition : ResourceDefinitionBase { - public AnotherDummyResourceDefinition() : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).AddResource().Build()) { } + public AnotherDummyResourceDefinition() : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).Add().Build()) { } } [Fact] @@ -66,7 +65,7 @@ public void HookDiscovery_InheritanceSubclass_CanDiscover() public class YetAnotherDummy : Identifiable { } public sealed class YetAnotherDummyResourceDefinition : ResourceDefinition { - public YetAnotherDummyResourceDefinition() : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).AddResource().Build()) { } + public YetAnotherDummyResourceDefinition() : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).Add().Build()) { } public override IEnumerable BeforeDelete(IResourceHashSet affected, ResourcePipeline pipeline) { return affected; } @@ -78,7 +77,7 @@ public override void AfterDelete(HashSet resources, ResourcePip public void HookDiscovery_WronglyUsedLoadDatabaseValueAttribute_ThrowsJsonApiSetupException() { // assert - Assert.Throws(() => + Assert.Throws(() => { // Arrange & act new HooksDiscovery(MockProvider(new YetAnotherDummyResourceDefinition())); @@ -98,7 +97,7 @@ public void HookDiscovery_InheritanceWithGenericSubclass_CanDiscover() public sealed class GenericDummyResourceDefinition : ResourceDefinition where TResource : class, IIdentifiable { - public GenericDummyResourceDefinition() : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).AddResource().Build()) { } + public GenericDummyResourceDefinition() : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).Add().Build()) { } public override IEnumerable BeforeDelete(IResourceHashSet resources, ResourcePipeline pipeline) { return resources; } public override void AfterDelete(HashSet resources, ResourcePipeline pipeline, bool succeeded) { } diff --git a/test/UnitTests/ResourceHooks/RelationshipDictionaryTests.cs b/test/UnitTests/ResourceHooks/RelationshipDictionaryTests.cs index 59ae2a621c..9873f7f4fb 100644 --- a/test/UnitTests/ResourceHooks/RelationshipDictionaryTests.cs +++ b/test/UnitTests/ResourceHooks/RelationshipDictionaryTests.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using JsonApiDotNetCore.Hooks; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; using Xunit; namespace UnitTests.ResourceHooks @@ -39,20 +39,23 @@ public sealed class RelationshipDictionaryTests public readonly HashSet AllResources; public RelationshipDictionaryTests() { - FirstToOneAttr = new HasOneAttribute("firstToOne") + FirstToOneAttr = new HasOneAttribute { + PublicName = "firstToOne", LeftType = typeof(Dummy), RightType = typeof(ToOne), Property = typeof(Dummy).GetProperty(nameof(Dummy.FirstToOne)) }; - SecondToOneAttr = new HasOneAttribute("secondToOne") + SecondToOneAttr = new HasOneAttribute { + PublicName = "secondToOne", LeftType = typeof(Dummy), RightType = typeof(ToOne), Property = typeof(Dummy).GetProperty(nameof(Dummy.SecondToOne)) }; - ToManyAttr = new HasManyAttribute("toManies") + ToManyAttr = new HasManyAttribute { + PublicName = "toManies", LeftType = typeof(Dummy), RightType = typeof(ToMany), Property = typeof(Dummy).GetProperty(nameof(Dummy.ToManies)) diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/AfterCreateTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/AfterCreateTests.cs index 4dafef66a4..a465f7dce6 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/AfterCreateTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/AfterCreateTests.cs @@ -1,7 +1,7 @@ -using JsonApiDotNetCore.Hooks; +using System.Collections.Generic; +using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCoreExample.Models; using Moq; -using System.Collections.Generic; using Xunit; namespace UnitTests.ResourceHooks diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreateTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreateTests.cs index 413ef00338..2f580128a9 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreateTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreateTests.cs @@ -1,7 +1,7 @@ -using JsonApiDotNetCore.Hooks; +using System.Collections.Generic; +using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCoreExample.Models; using Moq; -using System.Collections.Generic; using Xunit; namespace UnitTests.ResourceHooks diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreate_WithDbValues_Tests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreate_WithDbValues_Tests.cs index aa6acff4d1..2efe5e1391 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreate_WithDbValues_Tests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Create/BeforeCreate_WithDbValues_Tests.cs @@ -1,10 +1,10 @@ -using JsonApiDotNetCore.Hooks; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Microsoft.EntityFrameworkCore; using Moq; -using System.Collections.Generic; -using System.Linq; using Xunit; namespace UnitTests.ResourceHooks diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/AfterDeleteTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/AfterDeleteTests.cs index 0da8920138..af85d5d487 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/AfterDeleteTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/AfterDeleteTests.cs @@ -1,7 +1,7 @@ -using JsonApiDotNetCore.Hooks; +using System.Collections.Generic; +using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCoreExample.Models; using Moq; -using System.Collections.Generic; using Xunit; namespace UnitTests.ResourceHooks diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDeleteTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDeleteTests.cs index 9dfbe89174..b904846ec3 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDeleteTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDeleteTests.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore.Hooks; +using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCoreExample.Models; using Moq; using Xunit; diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDelete_WithDbValue_Tests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDelete_WithDbValue_Tests.cs index 4a9a8c1198..5500fb9c2a 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDelete_WithDbValue_Tests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/BeforeDelete_WithDbValue_Tests.cs @@ -1,10 +1,10 @@ -using JsonApiDotNetCore.Hooks; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Microsoft.EntityFrameworkCore; using Moq; -using System.Collections.Generic; -using System.Linq; using Xunit; namespace UnitTests.ResourceHooks diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/IdentifiableManyToMany_OnReturnTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/IdentifiableManyToMany_OnReturnTests.cs index 93332ce0ca..02b0e6a12d 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/IdentifiableManyToMany_OnReturnTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/IdentifiableManyToMany_OnReturnTests.cs @@ -1,8 +1,8 @@ -using JsonApiDotNetCore.Hooks; -using JsonApiDotNetCoreExample.Models; -using Moq; using System.Collections.Generic; using System.Linq; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCoreExample.Models; +using Moq; using Xunit; namespace UnitTests.ResourceHooks diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/ManyToMany_OnReturnTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/ManyToMany_OnReturnTests.cs index c4a2a1a6b6..e4db309781 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/ManyToMany_OnReturnTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/ManyToMany_OnReturnTests.cs @@ -1,8 +1,8 @@ -using JsonApiDotNetCore.Hooks; -using JsonApiDotNetCoreExample.Models; -using Moq; using System.Collections.Generic; using System.Linq; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCoreExample.Models; +using Moq; using Xunit; namespace UnitTests.ResourceHooks diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/BeforeReadTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/BeforeReadTests.cs index d2edc2797c..a967bc2c9d 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/BeforeReadTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/BeforeReadTests.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Hooks; +using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCore.Queries; using JsonApiDotNetCoreExample.Models; using Moq; diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/IdentifiableManyToMany_AfterReadTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/IdentifiableManyToMany_AfterReadTests.cs index c2a1fd8e1b..e91e3a6ebd 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/IdentifiableManyToMany_AfterReadTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/IdentifiableManyToMany_AfterReadTests.cs @@ -1,8 +1,8 @@ -using JsonApiDotNetCore.Hooks; -using JsonApiDotNetCoreExample.Models; -using Moq; using System.Collections.Generic; using System.Linq; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCoreExample.Models; +using Moq; using Xunit; namespace UnitTests.ResourceHooks diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/ManyToMany_AfterReadTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/ManyToMany_AfterReadTests.cs index 19039e2ff2..7ca674ea65 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/ManyToMany_AfterReadTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Read/ManyToMany_AfterReadTests.cs @@ -1,8 +1,8 @@ -using JsonApiDotNetCore.Hooks; -using JsonApiDotNetCoreExample.Models; -using Moq; using System.Collections.Generic; using System.Linq; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCoreExample.Models; +using Moq; using Xunit; namespace UnitTests.ResourceHooks diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/ScenarioTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/ScenarioTests.cs index cf05ddfae1..eb963e38f2 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/ScenarioTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/ScenarioTests.cs @@ -1,7 +1,7 @@ -using JsonApiDotNetCore.Hooks; +using System.Collections.Generic; +using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCoreExample.Models; using Moq; -using System.Collections.Generic; using Xunit; namespace UnitTests.ResourceHooks diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/AfterUpdateTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/AfterUpdateTests.cs index 3979a124e6..c4402b809c 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/AfterUpdateTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/AfterUpdateTests.cs @@ -1,7 +1,7 @@ -using JsonApiDotNetCore.Hooks; +using System.Collections.Generic; +using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCoreExample.Models; using Moq; -using System.Collections.Generic; using Xunit; namespace UnitTests.ResourceHooks diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdateTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdateTests.cs index 5c9d876058..7a299416c3 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdateTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdateTests.cs @@ -1,7 +1,7 @@ -using JsonApiDotNetCore.Hooks; +using System.Collections.Generic; +using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCoreExample.Models; using Moq; -using System.Collections.Generic; using Xunit; namespace UnitTests.ResourceHooks diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdate_WithDbValues_Tests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdate_WithDbValues_Tests.cs index 67b0aec733..46000c9318 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdate_WithDbValues_Tests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdate_WithDbValues_Tests.cs @@ -1,10 +1,10 @@ -using JsonApiDotNetCore.Hooks; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Microsoft.EntityFrameworkCore; using Moq; -using System.Collections.Generic; -using System.Linq; using Xunit; namespace UnitTests.ResourceHooks @@ -27,7 +27,7 @@ public BeforeUpdate_WithDbValues_Tests() var todoId = todoList[0].Id; var _personId = todoList[0].OneToOnePerson.Id; personId = _personId.ToString(); - var _implicitPersonId = (_personId + 10000); + var _implicitPersonId = _personId + 10000; var implicitTodo = _todoFaker.Generate(); implicitTodo.Id += 1000; @@ -82,7 +82,7 @@ public void BeforeUpdate_Deleting_Relationship() var personDiscovery = SetDiscoverableHooks(targetHooks, EnableDbValues); var (_, ufMock, hookExecutor, todoResourceMock, ownerResourceMock) = CreateTestObjects(todoDiscovery, personDiscovery, repoDbContextOptions: options); - ufMock.Setup(c => c.Relationships).Returns(_resourceGraph.GetRelationships((TodoItem t) => t.OneToOnePerson)); + ufMock.Setup(c => c.Relationships).Returns(_resourceGraph.GetRelationships((TodoItem t) => t.OneToOnePerson).ToList()); // Act var _todoList = new List { new TodoItem { Id = todoList[0].Id } }; @@ -210,7 +210,7 @@ private bool TodoCheckDiff(IDiffableResourceHashSet resources, string var getAffectedCheck = resources.GetAffected(e => e.OneToOnePerson).Any(); - return (dbCheck && reqCheck && diffCheck && getAffectedCheck); + return dbCheck && reqCheck && diffCheck && getAffectedCheck; } private bool TodoCheck(IRelationshipsDictionary rh, string checksum) diff --git a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs index e45e2d2c4a..8a1cfa0c24 100644 --- a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs +++ b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs @@ -1,26 +1,25 @@ -using Bogus; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Generics; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Hooks; -using JsonApiDotNetCoreExample.Data; -using JsonApiDotNetCoreExample.Models; -using Microsoft.EntityFrameworkCore; -using Moq; using System; using System.Collections.Generic; using System.Linq; -using Person = JsonApiDotNetCoreExample.Models.Person; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Models.Annotation; +using Bogus; +using JsonApiDotNetCore; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Hooks.Internal; +using JsonApiDotNetCore.Hooks.Internal.Discovery; +using JsonApiDotNetCore.Hooks.Internal.Execution; +using JsonApiDotNetCore.Hooks.Internal.Traversal; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Person = JsonApiDotNetCoreExample.Models.Person; namespace UnitTests.ResourceHooks { @@ -43,13 +42,13 @@ public HooksDummyData() var appDbContext = new AppDbContext(new DbContextOptionsBuilder().Options, new FrozenSystemClock()); _resourceGraph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance) - .AddResource() - .AddResource() - .AddResource() - .AddResource
() - .AddResource() - .AddResource() - .AddResource() + .Add() + .Add() + .Add() + .Add
() + .Add() + .Add() + .Add() .Build(); _todoFaker = new Faker().Rules((f, i) => i.Id = f.UniqueIndex + 1); @@ -173,7 +172,8 @@ public class HooksTestsSetup : HooksDummyData var execHelper = new HookExecutorHelper(gpfMock.Object, _resourceGraph, options); var traversalHelper = new TraversalHelper(_resourceGraph, ufMock.Object); - var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, constraintsMock.Object, _resourceGraph, null); + var resourceFactory = new Mock().Object; + var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, constraintsMock.Object, _resourceGraph, resourceFactory); return (constraintsMock, hookExecutor, primaryResource); } @@ -197,8 +197,8 @@ public class HooksTestsSetup : HooksDummyData var dbContext = repoDbContextOptions != null ? new AppDbContext(repoDbContextOptions, new FrozenSystemClock()) : null; var resourceGraph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance) - .AddResource() - .AddResource() + .Add() + .Add() .Build(); SetupProcessorFactoryForResourceDefinition(gpfMock, primaryResource.Object, primaryDiscovery, dbContext, resourceGraph); @@ -206,7 +206,8 @@ public class HooksTestsSetup : HooksDummyData var execHelper = new HookExecutorHelper(gpfMock.Object, _resourceGraph, options); var traversalHelper = new TraversalHelper(_resourceGraph, ufMock.Object); - var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, constraintsMock.Object, _resourceGraph, null); + var resourceFactory = new Mock().Object; + var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, constraintsMock.Object, _resourceGraph, resourceFactory); return (constraintsMock, ufMock, hookExecutor, primaryResource, secondaryResource); } @@ -233,9 +234,9 @@ public class HooksTestsSetup : HooksDummyData var dbContext = repoDbContextOptions != null ? new AppDbContext(repoDbContextOptions, new FrozenSystemClock()) : null; var resourceGraph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance) - .AddResource() - .AddResource() - .AddResource() + .Add() + .Add() + .Add() .Build(); SetupProcessorFactoryForResourceDefinition(gpfMock, primaryResource.Object, primaryDiscovery, dbContext, resourceGraph); @@ -244,7 +245,8 @@ public class HooksTestsSetup : HooksDummyData var execHelper = new HookExecutorHelper(gpfMock.Object, _resourceGraph, options); var traversalHelper = new TraversalHelper(_resourceGraph, ufMock.Object); - var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, constraintsMock.Object, _resourceGraph, null); + var resourceFactory = new Mock().Object; + var hookExecutor = new ResourceHookExecutor(execHelper, traversalHelper, ufMock.Object, constraintsMock.Object, _resourceGraph, resourceFactory); return (constraintsMock, hookExecutor, primaryResource, firstSecondaryResource, secondSecondaryResource); } @@ -368,7 +370,9 @@ private IResourceReadRepository CreateTestRepository(AppDbC var serviceProvider = ((IInfrastructure) dbContext).Instance; var resourceFactory = new ResourceFactory(serviceProvider); IDbContextResolver resolver = CreateTestDbResolver(dbContext); - return new EntityFrameworkCoreRepository(null, resolver, resourceGraph, null, resourceFactory, new List(), NullLoggerFactory.Instance); + var serviceFactory = new Mock().Object; + var targetedFields = new TargetedFields(); + return new EntityFrameworkCoreRepository(targetedFields, resolver, resourceGraph, serviceFactory, resourceFactory, new List(), NullLoggerFactory.Instance); } private IDbContextResolver CreateTestDbResolver(AppDbContext dbContext) where TModel : class, IIdentifiable diff --git a/test/UnitTests/Serialization/Client/RequestSerializerTests.cs b/test/UnitTests/Serialization/Client/RequestSerializerTests.cs index 261aaa4b61..a001b2c1f9 100644 --- a/test/UnitTests/Serialization/Client/RequestSerializerTests.cs +++ b/test/UnitTests/Serialization/Client/RequestSerializerTests.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; using System.Text.RegularExpressions; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Serialization.Client; -using Xunit; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Building; +using JsonApiDotNetCore.Serialization.Client.Internal; using UnitTests.TestModels; +using Xunit; namespace UnitTests.Serialization.Client { diff --git a/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs b/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs index 4138de80c6..c5d2d1d6ce 100644 --- a/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs +++ b/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs @@ -1,13 +1,12 @@ using System.Collections.Generic; using System.ComponentModel.Design; using System.Linq; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; -using JsonApiDotNetCore.Serialization.Client; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Client.Internal; +using JsonApiDotNetCore.Serialization.Objects; using Newtonsoft.Json; -using Xunit; using UnitTests.TestModels; +using Xunit; namespace UnitTests.Serialization.Client { @@ -77,7 +76,7 @@ public void DeserializeList_EmptyResponseWithTopLevelLinks_CanDeserialize() var body = JsonConvert.SerializeObject(content); // Act - var result = _deserializer.DeserializeList(body); + var result = _deserializer.DeserializeMany(body); // Assert Assert.Empty(result.Data); @@ -321,7 +320,7 @@ public void DeserializeList_DeeplyNestedIncluded_CanDeserialize() var body = JsonConvert.SerializeObject(content); // Act - var result = _deserializer.DeserializeList(body); + var result = _deserializer.DeserializeMany(body); var resource = result.Data.First(); // Assert diff --git a/test/UnitTests/Serialization/Common/DocumentBuilderTests.cs b/test/UnitTests/Serialization/Common/DocumentBuilderTests.cs index 857d0d200c..1023998c15 100644 --- a/test/UnitTests/Serialization/Common/DocumentBuilderTests.cs +++ b/test/UnitTests/Serialization/Common/DocumentBuilderTests.cs @@ -1,22 +1,23 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Building; +using JsonApiDotNetCore.Serialization.Objects; using Moq; -using Xunit; using UnitTests.TestModels; +using Xunit; namespace UnitTests.Serialization.Serializer { public sealed class BaseDocumentBuilderTests : SerializerTestsSetup { - private readonly TestDocumentBuilder _builder; + private readonly TestSerializer _builder; public BaseDocumentBuilderTests() { var mock = new Mock(); - mock.Setup(m => m.Build(It.IsAny(), It.IsAny>(), It.IsAny>())).Returns(new ResourceObject()); - _builder = new TestDocumentBuilder(mock.Object); + mock.Setup(m => m.Build(It.IsAny(), It.IsAny>(), It.IsAny>())).Returns(new ResourceObject()); + _builder = new TestSerializer(mock.Object); } diff --git a/test/UnitTests/Serialization/Common/DocumentParserTests.cs b/test/UnitTests/Serialization/Common/DocumentParserTests.cs index 1628dea0b7..09e10ab8c3 100644 --- a/test/UnitTests/Serialization/Common/DocumentParserTests.cs +++ b/test/UnitTests/Serialization/Common/DocumentParserTests.cs @@ -3,21 +3,21 @@ using System.Collections.Generic; using System.ComponentModel.Design; using System.Linq; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; using Newtonsoft.Json; -using Xunit; using UnitTests.TestModels; +using Xunit; namespace UnitTests.Serialization.Deserializer { public sealed class BaseDocumentParserTests : DeserializerTestsSetup { - private readonly TestDocumentParser _deserializer; + private readonly TestDeserializer _deserializer; public BaseDocumentParserTests() { - _deserializer = new TestDocumentParser(_resourceGraph, new ResourceFactory(new ServiceContainer())); + _deserializer = new TestDeserializer(_resourceGraph, new ResourceFactory(new ServiceContainer())); } [Fact] @@ -73,7 +73,7 @@ public void DeserializeResourceIdentifiers_ArrayData_CanDeserialize() var body = JsonConvert.SerializeObject(content); // Act - var result = (List)_deserializer.Deserialize(body); + var result = (IIdentifiable[])_deserializer.Deserialize(body); // Assert Assert.Equal("1", result.First().StringId); diff --git a/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs index 3d413ac962..58d380cee9 100644 --- a/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs +++ b/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs @@ -2,10 +2,10 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization; -using Xunit; +using JsonApiDotNetCore.Serialization.Building; +using JsonApiDotNetCore.Serialization.Objects; using UnitTests.TestModels; +using Xunit; namespace UnitTests.Serialization.Serializer { diff --git a/test/UnitTests/Serialization/DeserializerTestsSetup.cs b/test/UnitTests/Serialization/DeserializerTestsSetup.cs index 848b7d2546..95579b3f9f 100644 --- a/test/UnitTests/Serialization/DeserializerTestsSetup.cs +++ b/test/UnitTests/Serialization/DeserializerTestsSetup.cs @@ -1,9 +1,9 @@ -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization; using System.Collections.Generic; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Http; using Moq; @@ -18,13 +18,13 @@ public DeserializerTestsSetup() _mockHttpContextAccessor = new Mock(); _mockHttpContextAccessor.Setup(mock => mock.HttpContext).Returns(new DefaultHttpContext()); } - protected sealed class TestDocumentParser : BaseDocumentParser + protected sealed class TestDeserializer : BaseDeserializer { - public TestDocumentParser(IResourceGraph resourceGraph, IResourceFactory resourceFactory) : base(resourceGraph, resourceFactory) { } + public TestDeserializer(IResourceGraph resourceGraph, IResourceFactory resourceFactory) : base(resourceGraph, resourceFactory) { } - public new object Deserialize(string body) + public object Deserialize(string body) { - return base.Deserialize(body); + return DeserializeBody(body); } protected override void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipEntry data = null) { } diff --git a/test/UnitTests/Serialization/SerializationTestsSetupBase.cs b/test/UnitTests/Serialization/SerializationTestsSetupBase.cs index 2c3c85368f..ad51859960 100644 --- a/test/UnitTests/Serialization/SerializationTestsSetupBase.cs +++ b/test/UnitTests/Serialization/SerializationTestsSetupBase.cs @@ -1,7 +1,5 @@ using Bogus; -using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal.Contracts; using Microsoft.Extensions.Logging.Abstractions; using UnitTests.TestModels; using Person = UnitTests.TestModels.Person; @@ -40,25 +38,25 @@ public SerializationTestsSetupBase() protected IResourceGraph BuildGraph() { var resourceGraphBuilder = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance); - resourceGraphBuilder.AddResource("testResource"); - resourceGraphBuilder.AddResource("testResource-with-list"); + resourceGraphBuilder.Add("testResource"); + resourceGraphBuilder.Add("testResource-with-list"); // one to one relationships - resourceGraphBuilder.AddResource("oneToOnePrincipals"); - resourceGraphBuilder.AddResource("oneToOneDependents"); - resourceGraphBuilder.AddResource("oneToOneRequiredDependents"); + resourceGraphBuilder.Add("oneToOnePrincipals"); + resourceGraphBuilder.Add("oneToOneDependents"); + resourceGraphBuilder.Add("oneToOneRequiredDependents"); // one to many relationships - resourceGraphBuilder.AddResource("oneToManyPrincipals"); - resourceGraphBuilder.AddResource("oneToManyDependents"); - resourceGraphBuilder.AddResource("oneToMany-requiredDependents"); + resourceGraphBuilder.Add("oneToManyPrincipals"); + resourceGraphBuilder.Add("oneToManyDependents"); + resourceGraphBuilder.Add("oneToMany-requiredDependents"); // collective relationships - resourceGraphBuilder.AddResource("multiPrincipals"); - resourceGraphBuilder.AddResource("multiDependents"); + resourceGraphBuilder.Add("multiPrincipals"); + resourceGraphBuilder.Add("multiDependents"); - resourceGraphBuilder.AddResource
(); - resourceGraphBuilder.AddResource(); - resourceGraphBuilder.AddResource(); - resourceGraphBuilder.AddResource(); - resourceGraphBuilder.AddResource(); + resourceGraphBuilder.Add
(); + resourceGraphBuilder.Add(); + resourceGraphBuilder.Add(); + resourceGraphBuilder.Add(); + resourceGraphBuilder.Add(); return resourceGraphBuilder.Build(); } diff --git a/test/UnitTests/Serialization/SerializerTestsSetup.cs b/test/UnitTests/Serialization/SerializerTestsSetup.cs index 028668fe9b..840250f799 100644 --- a/test/UnitTests/Serialization/SerializerTestsSetup.cs +++ b/test/UnitTests/Serialization/SerializerTestsSetup.cs @@ -2,14 +2,13 @@ using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Models.JsonApiDocuments; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Serialization.Server; -using JsonApiDotNetCore.Serialization.Server.Builders; +using JsonApiDotNetCore.Serialization.Building; +using JsonApiDotNetCore.Serialization.Objects; using Moq; namespace UnitTests.Serialization @@ -117,16 +116,16 @@ protected IEnumerable GetIncludeConstraints(List - protected sealed class TestDocumentBuilder : BaseDocumentBuilder + protected sealed class TestSerializer : BaseSerializer { - public TestDocumentBuilder(IResourceObjectBuilder resourceObjectBuilder) : base(resourceObjectBuilder) { } + public TestSerializer(IResourceObjectBuilder resourceObjectBuilder) : base(resourceObjectBuilder) { } public new Document Build(IIdentifiable resource, IReadOnlyCollection attributes = null, IReadOnlyCollection relationships = null) { return base.Build(resource, attributes, relationships); } - public new Document Build(IEnumerable resources, IReadOnlyCollection attributes = null, IReadOnlyCollection relationships = null) + public new Document Build(IReadOnlyCollection resources, IReadOnlyCollection attributes = null, IReadOnlyCollection relationships = null) { return base.Build(resources, attributes, relationships); } diff --git a/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs index 5b3286221f..798f3b4f79 100644 --- a/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs +++ b/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs @@ -1,10 +1,9 @@ -using Xunit; using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Serialization.Server.Builders; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Building; using UnitTests.TestModels; -using Person = UnitTests.TestModels.Person; +using Xunit; namespace UnitTests.Serialization.Server { diff --git a/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs b/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs index 0712bdb24c..e2ca796928 100644 --- a/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs +++ b/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs @@ -1,17 +1,15 @@ using System.Collections.Generic; -using System.Net; using System.ComponentModel.Design; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using System.Net; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Serialization.Server; +using JsonApiDotNetCore.Serialization.Objects; using Moq; using Newtonsoft.Json; using Xunit; - namespace UnitTests.Serialization.Server { public sealed class RequestDeserializerTests : DeserializerTestsSetup diff --git a/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs index eecbad1aec..bc50c2ec93 100644 --- a/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs +++ b/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.Linq; -using JsonApiDotNetCore.Models.Annotation; -using Xunit; +using JsonApiDotNetCore.Resources.Annotations; using UnitTests.TestModels; +using Xunit; namespace UnitTests.Serialization.Server { @@ -13,7 +13,7 @@ public sealed class ResponseResourceObjectBuilderTests : SerializerTestsSetup public ResponseResourceObjectBuilderTests() { - _relationshipsForBuild = _resourceGraph.GetRelationships(e => new { e.Dependents }); + _relationshipsForBuild = _resourceGraph.GetRelationships(e => new { e.Dependents }).ToList(); } [Fact] diff --git a/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs b/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs index 0bb9459b61..665b4b9647 100644 --- a/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs +++ b/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs @@ -2,11 +2,11 @@ using System.Linq; using System.Net; using System.Text.RegularExpressions; -using JsonApiDotNetCore.Models.Annotation; -using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; using Newtonsoft.Json; -using Xunit; using UnitTests.TestModels; +using Xunit; namespace UnitTests.Serialization.Server { @@ -23,8 +23,7 @@ public void SerializeSingle_ResourceWithDefaultTargetFields_CanSerialize() string serialized = serializer.SerializeSingle(resource); // Assert - var expectedFormatted = - @"{ + var expectedFormatted = @"{ ""data"":{ ""type"":""testResource"", ""id"":""1"", @@ -57,8 +56,7 @@ public void SerializeMany_ResourceWithDefaultTargetFields_CanSerialize() string serialized = serializer.SerializeMany(new List { resource }); // Assert - var expectedFormatted = - @"{ + var expectedFormatted = @"{ ""data"":[{ ""type"":""testResource"", ""id"":""1"", @@ -96,8 +94,7 @@ public void SerializeSingle_ResourceWithIncludedRelationships_CanSerialize() string serialized = serializer.SerializeSingle(resource); // Assert - var expectedFormatted = - @"{ + var expectedFormatted = @"{ ""data"":{ ""type"":""multiPrincipals"", ""id"":""1"", @@ -153,14 +150,14 @@ public void SerializeSingle_ResourceWithDeeplyIncludedRelationships_CanSerialize }; var chains = _resourceGraph.GetRelationships() - .Select(r => - { - var chain = new List { r }; - if (r.PublicName != "populatedToManies") - return new List { r }; - chain.AddRange(_resourceGraph.GetRelationships()); - return chain; - }).ToList(); + .Select(r => + { + var chain = new List {r}; + if (r.PublicName != "populatedToManies") + return new List {r}; + chain.AddRange(_resourceGraph.GetRelationships()); + return chain; + }).ToList(); var serializer = GetResponseSerializer(inclusionChains: chains); @@ -168,8 +165,7 @@ public void SerializeSingle_ResourceWithDeeplyIncludedRelationships_CanSerialize string serialized = serializer.SerializeSingle(resource); // Assert - var expectedFormatted = - @"{ + var expectedFormatted = @"{ ""data"":{ ""type"":""multiPrincipals"", ""id"":""10"", @@ -269,14 +265,13 @@ public void SerializeSingle_ResourceWithLinksEnabled_CanSerialize() string serialized = serializer.SerializeSingle(resource); // Assert - var expectedFormatted = - @"{ + var expectedFormatted = @"{ ""links"":{ ""self"":""http://www.dummy.com/dummy-self-link"", - ""next"":""http://www.dummy.com/dummy-next-link"", - ""prev"":""http://www.dummy.com/dummy-prev-link"", ""first"":""http://www.dummy.com/dummy-first-link"", - ""last"":""http://www.dummy.com/dummy-last-link"" + ""last"":""http://www.dummy.com/dummy-last-link"", + ""prev"":""http://www.dummy.com/dummy-prev-link"", + ""next"":""http://www.dummy.com/dummy-next-link"" }, ""data"":{ ""type"":""oneToManyPrincipals"", @@ -314,8 +309,7 @@ public void SerializeSingle_ResourceWithMeta_IncludesMetaInResult() string serialized = serializer.SerializeSingle(resource); // Assert - var expectedFormatted = - @"{ + var expectedFormatted = @"{ ""meta"":{ ""test"": ""meta"" }, ""data"":{ ""type"":""oneToManyPrincipals"", @@ -341,15 +335,14 @@ public void SerializeSingle_NullWithLinksAndMeta_StillShowsLinksAndMeta() string serialized = serializer.SerializeSingle(null); // Assert - var expectedFormatted = - @"{ + var expectedFormatted = @"{ ""meta"":{ ""test"": ""meta"" }, ""links"":{ ""self"":""http://www.dummy.com/dummy-self-link"", - ""next"":""http://www.dummy.com/dummy-next-link"", - ""prev"":""http://www.dummy.com/dummy-prev-link"", ""first"":""http://www.dummy.com/dummy-first-link"", - ""last"":""http://www.dummy.com/dummy-last-link"" + ""last"":""http://www.dummy.com/dummy-last-link"", + ""prev"":""http://www.dummy.com/dummy-prev-link"", + ""next"":""http://www.dummy.com/dummy-next-link"" }, ""data"": null }"; @@ -390,8 +383,7 @@ public void SerializeSingleWithRequestRelationship_PopulatedToOneRelationship_Ca string serialized = serializer.SerializeSingle(resource); // Assert - var expectedFormatted = - @"{ + var expectedFormatted = @"{ ""data"":{ ""type"":""oneToOneDependents"", ""id"":""1"" @@ -436,8 +428,7 @@ public void SerializeSingleWithRequestRelationship_PopulatedToManyRelationship_C string serialized = serializer.SerializeSingle(resource); // Assert - var expectedFormatted = - @"{ + var expectedFormatted = @"{ ""data"":[{ ""type"":""oneToManyDependents"", ""id"":""1"" diff --git a/test/UnitTests/Services/DefaultResourceService_Tests.cs b/test/UnitTests/Services/DefaultResourceService_Tests.cs index 2bdaf42aab..986db2427e 100644 --- a/test/UnitTests/Services/DefaultResourceService_Tests.cs +++ b/test/UnitTests/Services/DefaultResourceService_Tests.cs @@ -3,15 +3,12 @@ using System.ComponentModel.Design; using System.Linq; using System.Threading.Tasks; -using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Data; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Internal.Queries; +using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.RequestServices; -using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Queries.Internal; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.Extensions.Logging.Abstractions; @@ -28,8 +25,8 @@ public sealed class JsonApiResourceServiceTests public JsonApiResourceServiceTests() { _resourceGraph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance) - .AddResource() - .AddResource() + .Add() + .Add() .Build(); } @@ -80,7 +77,7 @@ private JsonApiResourceService GetService() var resourceDefinitionProvider = new ResourceDefinitionProvider(_resourceGraph, new TestScopedServiceProvider(serviceProvider)); var paginationContext = new PaginationContext(); var composer = new QueryLayerComposer(new List(), _resourceGraph, resourceDefinitionProvider, options, paginationContext); - var currentRequest = new CurrentRequest + var request = new JsonApiRequest { PrimaryResource = _resourceGraph.GetResourceContext(), SecondaryResource = _resourceGraph.GetResourceContext(), @@ -89,7 +86,7 @@ private JsonApiResourceService GetService() }; return new JsonApiResourceService(_repositoryMock.Object, composer, paginationContext, options, - NullLoggerFactory.Instance, currentRequest, changeTracker, resourceFactory, null); + NullLoggerFactory.Instance, request, changeTracker, resourceFactory, null); } } } diff --git a/test/UnitTests/TestModels.cs b/test/UnitTests/TestModels.cs index 93bf2a08e2..28d36773b9 100644 --- a/test/UnitTests/TestModels.cs +++ b/test/UnitTests/TestModels.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.Annotation; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; namespace UnitTests.TestModels { @@ -14,7 +14,7 @@ public sealed class TestResource : Identifiable [Attr] public int? NullableIntField { get; set; } [Attr] public Guid GuidField { get; set; } [Attr] public ComplexType ComplexField { get; set; } - [Attr(AttrCapabilities.All & ~AttrCapabilities.AllowChange)] public string Immutable { get; set; } + [Attr(Capabilities = AttrCapabilities.All & ~AttrCapabilities.AllowChange)] public string Immutable { get; set; } } public class TestResourceWithList : Identifiable @@ -93,7 +93,7 @@ public class Article : Identifiable [HasOne] public Person Reviewer { get; set; } [HasOne] public Person Author { get; set; } - [HasOne(canInclude: false)] public Person CannotInclude { get; set; } + [HasOne(CanInclude = false)] public Person CannotInclude { get; set; } } public class Person : Identifiable diff --git a/test/UnitTests/TestScopedServiceProvider.cs b/test/UnitTests/TestScopedServiceProvider.cs index c770ce681e..207af5d59d 100644 --- a/test/UnitTests/TestScopedServiceProvider.cs +++ b/test/UnitTests/TestScopedServiceProvider.cs @@ -1,11 +1,11 @@ -using JsonApiDotNetCore.Services; +using System; +using JsonApiDotNetCore.Configuration; using Microsoft.AspNetCore.Http; using Moq; -using System; namespace UnitTests { - public sealed class TestScopedServiceProvider : IScopedServiceProvider + public sealed class TestScopedServiceProvider : IRequestScopedServiceProvider { private readonly IServiceProvider _serviceProvider; private readonly Mock _httpContextAccessorMock = new Mock();