diff --git a/README.md b/README.md index d0a5bb2cf2..3085181b1e 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ See [the documentation](https://json-api-dotnet.github.io/#/) for detailed usage ```csharp public class Article : Identifiable -{ +{ [Attr("name")] public string Name { get; set; } } @@ -91,7 +91,7 @@ Running tests locally requires access to a postgresql database. If you have docker installed, this can be propped up via: ```bash -docker run --rm --name jsonapi-dotnet-core-testing -e POSTGRES_DB=JsonApiDotNetCoreExample -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres +docker run --rm --name jsonapi-dotnet-core-testing -e POSTGRES_DB=JsonApiDotNetCoreExample -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres ``` And then to run the tests: diff --git a/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs b/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs index 05728321c3..1432afecd8 100644 --- a/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs +++ b/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs @@ -1,10 +1,11 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes.Exporters; using BenchmarkDotNet.Attributes.Jobs; +using System; namespace Benchmarks.LinkBuilder { - [MarkdownExporter, SimpleJob(launchCount : 3, warmupCount : 10, targetCount : 20), MemoryDiagnoser] + [MarkdownExporter, SimpleJob(launchCount: 3, warmupCount: 10, targetCount: 20), MemoryDiagnoser] public class LinkBuilder_GetNamespaceFromPath_Benchmarks { private const string PATH = "/api/some-really-long-namespace-path/resources/current/articles"; @@ -14,7 +15,7 @@ public class LinkBuilder_GetNamespaceFromPath_Benchmarks public void UsingSplit() => GetNamespaceFromPath_BySplitting(PATH, ENTITY_NAME); [Benchmark] - public void Current() => GetNameSpaceFromPath_Current(PATH, ENTITY_NAME); + public void Current() => GetNameSpaceFromPathCurrent(PATH, ENTITY_NAME); public static string GetNamespaceFromPath_BySplitting(string path, string entityName) { @@ -32,7 +33,38 @@ public static string GetNamespaceFromPath_BySplitting(string path, string entity return nSpace; } - public static string GetNameSpaceFromPath_Current(string path, string entityName) - => JsonApiDotNetCore.Builders.LinkBuilder.GetNamespaceFromPath(path, entityName); + public static string GetNameSpaceFromPathCurrent(string path, string entityName) + { + + var entityNameSpan = entityName.AsSpan(); + var pathSpan = path.AsSpan(); + const char delimiter = '/'; + for (var i = 0; i < pathSpan.Length; i++) + { + if (pathSpan[i].Equals(delimiter)) + { + var nextPosition = i + 1; + if (pathSpan.Length > i + entityNameSpan.Length) + { + var possiblePathSegment = pathSpan.Slice(nextPosition, entityNameSpan.Length); + if (entityNameSpan.SequenceEqual(possiblePathSegment)) + { + // check to see if it's the last position in the string + // or if the next character is a / + var lastCharacterPosition = nextPosition + entityNameSpan.Length; + + if (lastCharacterPosition == pathSpan.Length || pathSpan.Length >= lastCharacterPosition + 2 && pathSpan[lastCharacterPosition].Equals(delimiter)) + { + return pathSpan.Slice(0, i).ToString(); + } + } + } + } + } + + return string.Empty; + + + } } } diff --git a/benchmarks/Program.cs b/benchmarks/Program.cs index 9a2c45dffb..0ec4c80e14 100644 --- a/benchmarks/Program.cs +++ b/benchmarks/Program.cs @@ -1,4 +1,4 @@ -using BenchmarkDotNet.Running; +using BenchmarkDotNet.Running; using Benchmarks.JsonApiContext; using Benchmarks.LinkBuilder; using Benchmarks.Query; @@ -10,7 +10,7 @@ class Program { static void Main(string[] args) { var switcher = new BenchmarkSwitcher(new[] { typeof(JsonApiDeserializer_Benchmarks), - typeof(JsonApiSerializer_Benchmarks), + //typeof(JsonApiSerializer_Benchmarks), typeof(QueryParser_Benchmarks), typeof(LinkBuilder_GetNamespaceFromPath_Benchmarks), typeof(ContainsMediaTypeParameters_Benchmarks), diff --git a/benchmarks/Query/QueryParser_Benchmarks.cs b/benchmarks/Query/QueryParser_Benchmarks.cs index de82baa60f..19819c3609 100644 --- a/benchmarks/Query/QueryParser_Benchmarks.cs +++ b/benchmarks/Query/QueryParser_Benchmarks.cs @@ -5,6 +5,7 @@ using BenchmarkDotNet.Attributes.Jobs; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http.Internal; @@ -21,14 +22,14 @@ public class QueryParser_Benchmarks { private const string DESCENDING_SORT = "-" + ATTRIBUTE; public QueryParser_Benchmarks() { - var controllerContextMock = new Mock(); - controllerContextMock.Setup(m => m.RequestEntity).Returns(new ContextEntity { + var requestMock = new Mock(); + requestMock.Setup(m => m.GetContextEntity()).Returns(new ContextEntity { Attributes = new List { new AttrAttribute(ATTRIBUTE, ATTRIBUTE) } }); var options = new JsonApiOptions(); - _queryParser = new BenchmarkFacade(controllerContextMock.Object, options); + _queryParser = new BenchmarkFacade(requestMock.Object, options); } [Benchmark] @@ -58,8 +59,8 @@ private void Run(int iterations, Action action) { // this facade allows us to expose and micro-benchmark protected methods private class BenchmarkFacade : QueryParser { public BenchmarkFacade( - IControllerContext controllerContext, - JsonApiOptions options) : base(controllerContext, options) { } + IRequestManager requestManager, + JsonApiOptions options) : base(requestManager, options) { } public void _ParseSortParameters(string value) => base.ParseSortParameters(value); } diff --git a/benchmarks/Serialization/JsonApiSerializer_Benchmarks.cs b/benchmarks/Serialization/JsonApiSerializer_Benchmarks.cs index d80540434b..1238cc082f 100644 --- a/benchmarks/Serialization/JsonApiSerializer_Benchmarks.cs +++ b/benchmarks/Serialization/JsonApiSerializer_Benchmarks.cs @@ -1,49 +1,49 @@ -using System.Collections.Generic; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Attributes.Exporters; -using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal.Generics; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Services; -using Moq; -using Newtonsoft.Json.Serialization; - -namespace Benchmarks.Serialization { - [MarkdownExporter] - public class JsonApiSerializer_Benchmarks { - private const string TYPE_NAME = "simple-types"; - private static readonly SimpleType Content = new SimpleType(); - - private readonly JsonApiSerializer _jsonApiSerializer; - - public JsonApiSerializer_Benchmarks() { - var resourceGraphBuilder = new ResourceGraphBuilder(); - resourceGraphBuilder.AddResource(TYPE_NAME); - var resourceGraph = resourceGraphBuilder.Build(); - - var jsonApiContextMock = new Mock(); - jsonApiContextMock.SetupAllProperties(); - jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); - jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); - - var jsonApiOptions = new JsonApiOptions(); - jsonApiOptions.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); - jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - - var genericProcessorFactoryMock = new Mock(); - - var documentBuilder = new DocumentBuilder(jsonApiContextMock.Object); - _jsonApiSerializer = new JsonApiSerializer(jsonApiContextMock.Object, documentBuilder); - } - - [Benchmark] - public object SerializeSimpleObject() => _jsonApiSerializer.Serialize(Content); - - private class SimpleType : Identifiable { - [Attr("name")] - public string Name { get; set; } - } - } -} +//using System.Collections.Generic; +//using BenchmarkDotNet.Attributes; +//using BenchmarkDotNet.Attributes.Exporters; +//using JsonApiDotNetCore.Builders; +//using JsonApiDotNetCore.Configuration; +//using JsonApiDotNetCore.Internal.Generics; +//using JsonApiDotNetCore.Models; +//using JsonApiDotNetCore.Serialization; +//using JsonApiDotNetCore.Services; +//using Moq; +//using Newtonsoft.Json.Serialization; + +//namespace Benchmarks.Serialization { +// [MarkdownExporter] +// public class JsonApiSerializer_Benchmarks { +// private const string TYPE_NAME = "simple-types"; +// private static readonly SimpleType Content = new SimpleType(); + +// private readonly JsonApiSerializer _jsonApiSerializer; + +// public JsonApiSerializer_Benchmarks() { +// var resourceGraphBuilder = new ResourceGraphBuilder(); +// resourceGraphBuilder.AddResource(TYPE_NAME); +// var resourceGraph = resourceGraphBuilder.Build(); + +// var jsonApiContextMock = new Mock(); +// jsonApiContextMock.SetupAllProperties(); +// jsonApiContextMock.Setup(m => m.ResourceGraph).Returns(resourceGraph); +// jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); + +// var jsonApiOptions = new JsonApiOptions(); +// jsonApiOptions.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); +// jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); + +// var genericProcessorFactoryMock = new Mock(); + +// var documentBuilder = new DocumentBuilder(jsonApiContextMock.Object); +// _jsonApiSerializer = new JsonApiSerializer(jsonApiContextMock.Object, documentBuilder); +// } + +// [Benchmark] +// public object SerializeSimpleObject() => _jsonApiSerializer.Serialize(Content); + +// private class SimpleType : Identifiable { +// [Attr("name")] +// public string Name { get; set; } +// } +// } +//} diff --git a/docs/obsoletes.md b/docs/obsoletes.md new file mode 100644 index 0000000000..31a60249f2 --- /dev/null +++ b/docs/obsoletes.md @@ -0,0 +1,3 @@ +# For v5 + +* Anything to do with JsonApiContext, make it internal and fix anything related to it. \ No newline at end of file diff --git a/markdownlint.config b/markdownlint.config new file mode 100644 index 0000000000..8f376b5c2a --- /dev/null +++ b/markdownlint.config @@ -0,0 +1,5 @@ +{ + "MD033": { + "allowed_elements": [ "p", "img", "p" ] + } +} \ No newline at end of file diff --git a/src/Examples/GettingStarted/Controllers/ArticlesController.cs b/src/Examples/GettingStarted/Controllers/ArticlesController.cs index 53517540b1..2d1567659b 100644 --- a/src/Examples/GettingStarted/Controllers/ArticlesController.cs +++ b/src/Examples/GettingStarted/Controllers/ArticlesController.cs @@ -1,5 +1,7 @@ using GettingStarted.Models; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; namespace GettingStarted @@ -7,9 +9,10 @@ namespace GettingStarted public class ArticlesController : JsonApiController
{ public ArticlesController( - IJsonApiContext jsonApiContext, - IResourceService
resourceService) - : base(jsonApiContext, resourceService) + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, + IResourceService
resourceService) + : base(jsonApiOptions, resourceGraph, resourceService) { } } -} \ No newline at end of file +} diff --git a/src/Examples/GettingStarted/Controllers/PeopleController.cs b/src/Examples/GettingStarted/Controllers/PeopleController.cs index f3c0c4b868..8d8ee58a2a 100644 --- a/src/Examples/GettingStarted/Controllers/PeopleController.cs +++ b/src/Examples/GettingStarted/Controllers/PeopleController.cs @@ -1,5 +1,7 @@ using GettingStarted.Models; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; namespace GettingStarted @@ -7,9 +9,10 @@ namespace GettingStarted public class PeopleController : JsonApiController { public PeopleController( - IJsonApiContext jsonApiContext, - IResourceService resourceService) - : base(jsonApiContext, resourceService) + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, + IResourceService resourceService) + : base(jsonApiOptions, resourceGraph, resourceService) { } } -} \ No newline at end of file +} diff --git a/src/Examples/GettingStarted/Properties/launchSettings.json b/src/Examples/GettingStarted/Properties/launchSettings.json index 7dc07f5b4f..be3b59bd69 100644 --- a/src/Examples/GettingStarted/Properties/launchSettings.json +++ b/src/Examples/GettingStarted/Properties/launchSettings.json @@ -1,23 +1,27 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:56042/", - "sslPort": 0 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:56042/", + "sslPort": 0 + } }, - "GettingStarted": { - "commandName": "Project", - "launchBrowser": true + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "GettingStarted": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:49300/" + } } - } -} +} \ No newline at end of file diff --git a/src/Examples/GettingStarted/ResourceDefinitionExample/ModelDefinition.cs b/src/Examples/GettingStarted/ResourceDefinitionExample/ModelDefinition.cs index e9581fc401..58a4acf41d 100644 --- a/src/Examples/GettingStarted/ResourceDefinitionExample/ModelDefinition.cs +++ b/src/Examples/GettingStarted/ResourceDefinitionExample/ModelDefinition.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; namespace GettingStarted.ResourceDefinitionExample @@ -18,4 +19,4 @@ public ModelDefinition(IResourceGraph graph) : base(graph) protected override List OutputAttrs() => Remove(model => model.DontExpose); } -} \ No newline at end of file +} diff --git a/src/Examples/GettingStarted/ResourceDefinitionExample/ModelsController.cs b/src/Examples/GettingStarted/ResourceDefinitionExample/ModelsController.cs index a14394e830..1c066c28c9 100644 --- a/src/Examples/GettingStarted/ResourceDefinitionExample/ModelsController.cs +++ b/src/Examples/GettingStarted/ResourceDefinitionExample/ModelsController.cs @@ -1,5 +1,6 @@ -using GettingStarted.Models; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; namespace GettingStarted.ResourceDefinitionExample @@ -7,9 +8,10 @@ namespace GettingStarted.ResourceDefinitionExample public class ModelsController : JsonApiController { public ModelsController( - IJsonApiContext jsonApiContext, - IResourceService resourceService) - : base(jsonApiContext, resourceService) + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, + IResourceService resourceService) + : base(jsonApiOptions, resourceGraph, resourceService) { } } -} \ No newline at end of file +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs index 95aa7d69f9..abf6ce50ea 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs @@ -1,4 +1,6 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; @@ -7,9 +9,10 @@ namespace JsonApiDotNetCoreExample.Controllers public class ArticlesController : JsonApiController
{ public ArticlesController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService
resourceService) - : base(jsonApiContext, resourceService) + : base(jsonApiOptions, resourceGraph, resourceService) { } } -} \ No newline at end of file +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs index e46b3f8efd..c46a9aa094 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs @@ -1,4 +1,6 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Mvc; @@ -11,10 +13,11 @@ namespace JsonApiDotNetCoreExample.Controllers public class CamelCasedModelsController : JsonApiController { public CamelCasedModelsController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs index 28a47eb419..5c6cab290d 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs @@ -8,8 +8,8 @@ public class PassportsController : JsonApiController { public PassportsController( IJsonApiContext jsonApiContext, - IResourceService resourceService) + IResourceService resourceService) : base(jsonApiContext, resourceService) { } } -} \ No newline at end of file +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs index e249e2af53..d29cafe508 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs @@ -1,4 +1,6 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.Extensions.Logging; @@ -8,10 +10,11 @@ namespace JsonApiDotNetCoreExample.Controllers public class PeopleController : JsonApiController { public PeopleController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/PersonRolesController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/PersonRolesController.cs index dbc3b482f5..0234eb6899 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/PersonRolesController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/PersonRolesController.cs @@ -1,4 +1,6 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.Extensions.Logging; @@ -8,10 +10,11 @@ namespace JsonApiDotNetCoreExample.Controllers public class PersonRolesController : JsonApiController { public PersonRolesController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs index 6a27038191..56bc6fda48 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs @@ -1,8 +1,10 @@ using System; using System.Linq; using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Mvc; @@ -13,18 +15,17 @@ namespace JsonApiDotNetCoreExample.Controllers { public class TodoCollectionsController : JsonApiController { - readonly IDbContextResolver _dbResolver; - public TodoCollectionsController( - IDbContextResolver contextResolver, - IJsonApiContext jsonApiContext, - IResourceService resourceService, - ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + public TodoCollectionsController( + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, + IDbContextResolver contextResolver, + IResourceService resourceService, + ILoggerFactory loggerFactory) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) { _dbResolver = contextResolver; - } [HttpPatch("{id}")] @@ -40,4 +41,4 @@ public override async Task PatchAsync(Guid id, [FromBody] TodoIte } } -} \ No newline at end of file +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs index 768dd1c37c..c7f3a6244e 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs @@ -1,4 +1,6 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.Extensions.Logging; @@ -8,10 +10,11 @@ namespace JsonApiDotNetCoreExample.Controllers public class TodoItemsController : JsonApiController { public TodoItemsController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOPtions, + IResourceGraph resourceGraph, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOPtions, resourceGraph, resourceService, loggerFactory) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs index ca2e860fa9..17b11215c4 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs @@ -9,8 +9,7 @@ namespace JsonApiDotNetCoreExample.Controllers { - [DisableRoutingConvention] - [Route("custom/route/todo-items")] + [DisableRoutingConvention, Route("custom/route/todo-items")] public class TodoItemsCustomController : CustomJsonApiController { public TodoItemsCustomController( diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs index 9bab3cf544..18e566dc07 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs @@ -1,4 +1,6 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; @@ -11,10 +13,11 @@ public abstract class AbstractTodoItemsController : JsonApiController where T : class, IIdentifiable { protected AbstractTodoItemsController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService service, ILoggerFactory loggerFactory) - : base(jsonApiContext, service, loggerFactory) + : base(jsonApiOptions, resourceGraph, service, loggerFactory) { } } @@ -22,10 +25,11 @@ protected AbstractTodoItemsController( public class TodoItemsTestController : AbstractTodoItemsController { public TodoItemsTestController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService service, ILoggerFactory loggerFactory) - : base(jsonApiContext, service, loggerFactory) + : base(jsonApiOptions, resourceGraph, service, loggerFactory) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs index dbd144caa4..475b93b300 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs @@ -1,4 +1,6 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.Extensions.Logging; @@ -8,10 +10,11 @@ namespace JsonApiDotNetCoreExample.Controllers public class UsersController : JsonApiController { public UsersController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/ArticleResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/ArticleResource.cs index 66429f175c..17dfca12f2 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/ArticleResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/ArticleResource.cs @@ -7,6 +7,7 @@ using JsonApiDotNetCoreExample.Models; using Microsoft.Extensions.Logging; using System.Security.Principal; +using JsonApiDotNetCore.Internal.Contracts; namespace JsonApiDotNetCoreExample.Resources { diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResource.cs index 7ad9659f18..59af8be86b 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResource.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCoreExample.Models; diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/PassportResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/PassportResource.cs index 457468c484..ba8b722277 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/PassportResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/PassportResource.cs @@ -5,6 +5,7 @@ using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Hooks; using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCore.Internal.Contracts; namespace JsonApiDotNetCoreExample.Resources { diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/PersonResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/PersonResource.cs index 4887414e73..ef962ed901 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/PersonResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/PersonResource.cs @@ -3,6 +3,7 @@ using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Hooks; using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCore.Internal.Contracts; namespace JsonApiDotNetCoreExample.Resources { diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/TagResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/TagResource.cs index e3b3100ddd..cd266303f3 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/TagResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/TagResource.cs @@ -4,14 +4,13 @@ using JsonApiDotNetCore.Hooks; using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; namespace JsonApiDotNetCoreExample.Resources { public class TagResource : ResourceDefinition { - public TagResource(IResourceGraph graph) : base(graph) - { - } + public TagResource(IResourceGraph graph) : base(graph) { } public override IEnumerable BeforeCreate(IEntityHashSet affected, ResourcePipeline pipeline) { diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/TodoResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/TodoResource.cs index cfba9855d3..1c9f89a7fc 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/TodoResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/TodoResource.cs @@ -4,6 +4,7 @@ using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Hooks; using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCore.Internal.Contracts; namespace JsonApiDotNetCoreExample.Resources { diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs index ec54b6144e..dade154eae 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs @@ -4,6 +4,7 @@ using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; namespace JsonApiDotNetCoreExample.Resources { diff --git a/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs b/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs new file mode 100644 index 0000000000..66286d2df5 --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs @@ -0,0 +1,41 @@ + +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreExample.Models; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace JsonApiDotNetCoreExample.Services +{ + public class CustomArticleService : EntityResourceService
+ { + public CustomArticleService( + IEntityRepository
repository, + IJsonApiOptions jsonApiOptions, + IRequestManager queryManager, + IPageManager pageManager, + IResourceGraph resourceGraph, + ILoggerFactory loggerFactory = null + ) : base(repository: repository, jsonApiOptions, queryManager, pageManager, resourceGraph:resourceGraph, loggerFactory) + { } + + public override async Task
GetAsync(int id) + { + var newEntity = await base.GetAsync(id); + if(newEntity == null) + { + throw new JsonApiException(404, "The entity could not be found"); + } + newEntity.Name = "None for you Glen Coco"; + return newEntity; + } + } + +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startup.cs index 44784f7eac..1b3af9a1db 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startup.cs @@ -7,6 +7,9 @@ using Microsoft.EntityFrameworkCore; using JsonApiDotNetCore.Extensions; using System; +using System.ComponentModel.Design; +using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCore.Services; namespace JsonApiDotNetCoreExample { @@ -29,9 +32,7 @@ public virtual IServiceProvider ConfigureServices(IServiceCollection services) { var loggerFactory = new LoggerFactory(); loggerFactory.AddConsole(LogLevel.Warning); - var mvcBuilder = services.AddMvcCore(); - services .AddSingleton(loggerFactory) .AddDbContext(options => options.UseNpgsql(GetDbConnectionString()), ServiceLifetime.Transient) @@ -55,9 +56,7 @@ public virtual void Configure( AppDbContext context) { context.Database.EnsureCreated(); - loggerFactory.AddConsole(Config.GetSection("Logging")); - app.UseJsonApi(); } diff --git a/src/Examples/NoEntityFrameworkExample/Controllers/CustomTodoItemsController.cs b/src/Examples/NoEntityFrameworkExample/Controllers/CustomTodoItemsController.cs index a6ded9749f..4e93d26bea 100644 --- a/src/Examples/NoEntityFrameworkExample/Controllers/CustomTodoItemsController.cs +++ b/src/Examples/NoEntityFrameworkExample/Controllers/CustomTodoItemsController.cs @@ -1,4 +1,6 @@ -using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.Extensions.Logging; @@ -8,10 +10,11 @@ namespace NoEntityFrameworkExample.Controllers public class CustomTodoItemsController : JsonApiController { public CustomTodoItemsController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) { } } } diff --git a/src/Examples/ReportsExample/Controllers/ReportsController.cs b/src/Examples/ReportsExample/Controllers/ReportsController.cs index 6f431d9291..e421477c20 100644 --- a/src/Examples/ReportsExample/Controllers/ReportsController.cs +++ b/src/Examples/ReportsExample/Controllers/ReportsController.cs @@ -1,17 +1,20 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; - +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal.Contracts; + namespace ReportsExample.Controllers { [Route("api/[controller]")] public class ReportsController : BaseJsonApiController { - public ReportsController( - IJsonApiContext jsonApiContext, + public ReportsController( + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IGetAllService getAll) - : base(jsonApiContext, getAll: getAll) + : base(jsonApiOptions, resourceGraph, getAll: getAll) { } [HttpGet] diff --git a/src/Examples/ResourceEntitySeparationExample/Controllers/CoursesController.cs b/src/Examples/ResourceEntitySeparationExample/Controllers/CoursesController.cs index 6809ace0bb..48d280f5cb 100644 --- a/src/Examples/ResourceEntitySeparationExample/Controllers/CoursesController.cs +++ b/src/Examples/ResourceEntitySeparationExample/Controllers/CoursesController.cs @@ -1,4 +1,6 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models.Resources; using Microsoft.Extensions.Logging; @@ -8,10 +10,11 @@ namespace ResourceEntitySeparationExample.Controllers public class CoursesController : JsonApiController { public CoursesController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) { } } } diff --git a/src/Examples/ResourceEntitySeparationExample/Controllers/DepartmentsController.cs b/src/Examples/ResourceEntitySeparationExample/Controllers/DepartmentsController.cs index 08f3ab33ad..63310743ac 100644 --- a/src/Examples/ResourceEntitySeparationExample/Controllers/DepartmentsController.cs +++ b/src/Examples/ResourceEntitySeparationExample/Controllers/DepartmentsController.cs @@ -1,5 +1,7 @@ using System; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models.Resources; using Microsoft.Extensions.Logging; @@ -9,10 +11,11 @@ namespace ResourceEntitySeparationExample.Controllers public class DepartmentsController : JsonApiController { public DepartmentsController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) { } } } diff --git a/src/Examples/ResourceEntitySeparationExample/Controllers/StudentsController.cs b/src/Examples/ResourceEntitySeparationExample/Controllers/StudentsController.cs index 34d5d33031..5f3551849a 100644 --- a/src/Examples/ResourceEntitySeparationExample/Controllers/StudentsController.cs +++ b/src/Examples/ResourceEntitySeparationExample/Controllers/StudentsController.cs @@ -1,4 +1,6 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models.Resources; using Microsoft.Extensions.Logging; @@ -8,10 +10,11 @@ namespace ResourceEntitySeparationExample.Controllers public class StudentsController : JsonApiController { public StudentsController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService resourceService, ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) { } } } diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index ad52aeb8bb..d8c7118ae8 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -4,6 +4,8 @@ using System.Linq; using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; @@ -12,6 +14,8 @@ namespace JsonApiDotNetCore.Builders /// public class DocumentBuilder : IDocumentBuilder { + private readonly IRequestManager _requestManager; + private readonly IPageManager _pageManager; private readonly IJsonApiContext _jsonApiContext; private readonly IResourceGraph _resourceGraph; private readonly IRequestMeta _requestMeta; @@ -20,10 +24,14 @@ public class DocumentBuilder : IDocumentBuilder public DocumentBuilder( IJsonApiContext jsonApiContext, + IPageManager pageManager, + IRequestManager requestManager, IRequestMeta requestMeta = null, IDocumentBuilderOptionsProvider documentBuilderOptionsProvider = null, IScopedServiceProvider scopedServiceProvider = null) { + _requestManager = requestManager; + _pageManager = pageManager; _jsonApiContext = jsonApiContext; _resourceGraph = jsonApiContext.ResourceGraph; _requestMeta = requestMeta; @@ -44,7 +52,9 @@ public Document Build(IIdentifiable entity) }; if (ShouldIncludePageLinks(contextEntity)) - document.Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext)); + { + document.Links = _pageManager.GetPageLinks(); + } document.Included = AppendIncludedObject(document.Included, contextEntity, entity); @@ -66,7 +76,9 @@ public Documents Build(IEnumerable entities) }; if (ShouldIncludePageLinks(contextEntity)) - documents.Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext)); + { + documents.Links = _pageManager.GetPageLinks(); + } foreach (var entity in enumeratedEntities) { @@ -80,7 +92,7 @@ public Documents Build(IEnumerable entities) private Dictionary GetMeta(IIdentifiable entity) { var builder = _jsonApiContext.MetaBuilder; - if (_jsonApiContext.Options.IncludeTotalRecordCount && _jsonApiContext.PageManager.TotalRecords != null) + if (_jsonApiContext.Options.IncludeTotalRecordCount && _pageManager.TotalRecords != null) builder.Add("total-records", _jsonApiContext.PageManager.TotalRecords); if (_requestMeta != null) @@ -146,9 +158,9 @@ private bool ShouldIncludeAttribute(AttrAttribute attr, object attributeValue, R { return OmitNullValuedAttribute(attr, attributeValue) == false && attr.InternalAttributeName != nameof(Identifiable.Id) - && ((_jsonApiContext.QuerySet == null - || _jsonApiContext.QuerySet.Fields.Count == 0) - || _jsonApiContext.QuerySet.Fields.Contains(relationship != null ? + && ((_requestManager.QuerySet == null + || _requestManager.QuerySet.Fields.Count == 0) + || _requestManager.QuerySet.Fields.Contains(relationship != null ? $"{relationship.InternalRelationshipName}.{attr.InternalAttributeName}" : attr.InternalAttributeName)); } @@ -171,7 +183,7 @@ private void AddRelationships(ResourceObject data, ContextEntity contextEntity, private RelationshipData GetRelationshipData(RelationshipAttribute attr, ContextEntity contextEntity, IIdentifiable entity) { - var linkBuilder = new LinkBuilder(_jsonApiContext); + var linkBuilder = new LinkBuilder(_jsonApiContext.Options,_requestManager); var relationshipData = new RelationshipData(); @@ -179,10 +191,14 @@ private RelationshipData GetRelationshipData(RelationshipAttribute attr, Context { relationshipData.Links = new Links(); if (attr.DocumentLinks.HasFlag(Link.Self)) + { relationshipData.Links.Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.StringId, attr.PublicRelationshipName); + } if (attr.DocumentLinks.HasFlag(Link.Related)) + { relationshipData.Links.Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.StringId, attr.PublicRelationshipName); + } } // this only includes the navigation property, we need to actually check the navigation property Id @@ -201,9 +217,9 @@ private RelationshipData GetRelationshipData(RelationshipAttribute attr, Context private List GetIncludedEntities(List included, ContextEntity rootContextEntity, IIdentifiable rootResource) { - if (_jsonApiContext.IncludedRelationships != null) + if (_requestManager.IncludedRelationships != null) { - foreach (var relationshipName in _jsonApiContext.IncludedRelationships) + foreach (var relationshipName in _jsonApiContext.RequestManager.IncludedRelationships) { var relationshipChain = relationshipName.Split('.'); diff --git a/src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs index 45ba096447..db20954730 100644 --- a/src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.Builders public interface IDocumentBuilder { /// - /// Builds a json:api document from the provided resource instance. + /// Builds a Json:Api document from the provided resource instance. /// /// The resource to convert. Document Build(IIdentifiable entity); diff --git a/src/JsonApiDotNetCore/Builders/ILinkBuilder.cs b/src/JsonApiDotNetCore/Builders/ILinkBuilder.cs new file mode 100644 index 0000000000..c8af9e7dac --- /dev/null +++ b/src/JsonApiDotNetCore/Builders/ILinkBuilder.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Http; + +namespace JsonApiDotNetCore.Builders +{ + public interface ILinkBuilder + { + string GetPageLink(int pageOffset, int pageSize); + string GetRelatedRelationLink(string parent, string parentId, string child); + string GetSelfRelationLink(string parent, string parentId, string child); + } +} diff --git a/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs new file mode 100644 index 0000000000..7bcf6aa7ad --- /dev/null +++ b/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Graph; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCore.Builders +{ + public interface IResourceGraphBuilder + { + /// + /// Construct the + /// + IResourceGraph Build(); + + /// + /// Add a json:api resource + /// + /// The resource model type + /// + /// The pluralized name that should be exposed by the API. + /// If nothing is specified, the configured name formatter will be used. + /// See . + /// + IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable; + + IResourceGraphBuilder AddControllerPairing(Type controller, Type model = null); + + /// + /// Add a json:api resource + /// + /// The resource model type + /// The resource model identifier type + /// + /// The pluralized name that should be exposed by the API. + /// If nothing is specified, the configured name formatter will be used. + /// See . + /// + IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable; + + /// + /// Add a Json:Api resource + /// + /// The resource model type + /// The resource model identifier type + /// + /// The pluralized name that should be exposed by the API. + /// If nothing is specified, the configured name formatter will be used. + /// See . + /// + IResourceGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName = null); + + /// + /// Add all the models that are part of the provided + /// that also implement + /// + /// The implementation type. + IResourceGraphBuilder AddDbContext() where T : DbContext; + + /// + /// Specify the used to format resource names. + /// + /// Formatter used to define exposed resource names by convention. + IResourceGraphBuilder UseNameFormatter(IResourceNameFormatter resourceNameFormatter); + + /// + /// Which links to include. Defaults to . + /// + Link DocumentLinks { get; set; } + + + } +} diff --git a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs index 3de45558c4..f06134d397 100644 --- a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs @@ -1,72 +1,38 @@ using System; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; namespace JsonApiDotNetCore.Builders { - public class LinkBuilder + public class LinkBuilder : ILinkBuilder { - private readonly IJsonApiContext _context; + private IRequestManager _requestManager; + private IJsonApiOptions _options; - public LinkBuilder(IJsonApiContext context) + public LinkBuilder(IJsonApiOptions options, IRequestManager requestManager) { - _context = context; + _requestManager = requestManager; + _options = options; } - public string GetBasePath(HttpContext context, string entityName) - { - var r = context.Request; - return (_context.Options.RelativeLinks) - ? GetNamespaceFromPath(r.Path, entityName) - : $"{r.Scheme}://{r.Host}{GetNamespaceFromPath(r.Path, entityName)}"; - } - - internal static string GetNamespaceFromPath(string path, string entityName) - { - var entityNameSpan = entityName.AsSpan(); - var pathSpan = path.AsSpan(); - const char delimiter = '/'; - for (var i = 0; i < pathSpan.Length; i++) - { - if(pathSpan[i].Equals(delimiter)) - { - var nextPosition = i + 1; - if(pathSpan.Length > i + entityNameSpan.Length) - { - var possiblePathSegment = pathSpan.Slice(nextPosition, entityNameSpan.Length); - if (entityNameSpan.SequenceEqual(possiblePathSegment)) - { - // check to see if it's the last position in the string - // or if the next character is a / - var lastCharacterPosition = nextPosition + entityNameSpan.Length; - - if(lastCharacterPosition == pathSpan.Length || pathSpan.Length >= lastCharacterPosition + 2 && pathSpan[lastCharacterPosition].Equals(delimiter)) - { - return pathSpan.Slice(0, i).ToString(); - } - } - } - } - } - - return string.Empty; - } public string GetSelfRelationLink(string parent, string parentId, string child) { - return $"{_context.BasePath}/{parent}/{parentId}/relationships/{child}"; + return $"{_requestManager.BasePath}/{parent}/{parentId}/relationships/{child}"; } public string GetRelatedRelationLink(string parent, string parentId, string child) { - return $"{_context.BasePath}/{parent}/{parentId}/{child}"; + return $"{_requestManager.BasePath}/{parent}/{parentId}/{child}"; } public string GetPageLink(int pageOffset, int pageSize) { var filterQueryComposer = new QueryComposer(); - var filters = filterQueryComposer.Compose(_context); - return $"{_context.BasePath}/{_context.RequestEntity.EntityName}?page[size]={pageSize}&page[number]={pageOffset}{filters}"; + var filters = filterQueryComposer.Compose(_requestManager); + return $"{_requestManager.BasePath}/{_requestManager.GetContextEntity().EntityName}?page[size]={pageSize}&page[number]={pageOffset}{filters}"; } } } diff --git a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs similarity index 78% rename from src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs rename to src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs index dfa6b665a0..500446a7c5 100644 --- a/src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs @@ -7,77 +7,20 @@ using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Builders { - public interface IResourceGraphBuilder - { - /// - /// Construct the - /// - IResourceGraph Build(); - - /// - /// Add a json:api resource - /// - /// The resource model type - /// - /// The pluralized name that should be exposed by the API. - /// If nothing is specified, the configured name formatter will be used. - /// See . - /// - IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable; - - /// - /// Add a json:api resource - /// - /// The resource model type - /// The resource model identifier type - /// - /// The pluralized name that should be exposed by the API. - /// If nothing is specified, the configured name formatter will be used. - /// See . - /// - IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable; - - /// - /// Add a json:api resource - /// - /// The resource model type - /// The resource model identifier type - /// - /// The pluralized name that should be exposed by the API. - /// If nothing is specified, the configured name formatter will be used. - /// See . - /// - IResourceGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName = null); - - /// - /// Add all the models that are part of the provided - /// that also implement - /// - /// The implementation type. - IResourceGraphBuilder AddDbContext() where T : DbContext; - - /// - /// Specify the used to format resource names. - /// - /// Formatter used to define exposed resource names by convention. - IResourceGraphBuilder UseNameFormatter(IResourceNameFormatter resourceNameFormatter); - - /// - /// Which links to include. Defaults to . - /// - Link DocumentLinks { get; set; } - } - public class ResourceGraphBuilder : IResourceGraphBuilder { private List _entities = new List(); private List _validationResults = new List(); + private Dictionary> _controllerMapper = new Dictionary>() { }; + private List _undefinedMapper = new List() { }; private bool _usesDbContext; private IResourceNameFormatter _resourceNameFormatter = JsonApiOptions.ResourceNameFormatter; @@ -90,7 +33,23 @@ public IResourceGraph Build() // this must be done at build so that call order doesn't matter _entities.ForEach(e => e.Links = GetLinkFlags(e.EntityType)); - var graph = new ResourceGraph(_entities, _usesDbContext, _validationResults); + List controllerContexts = new List() { }; + foreach(var cm in _controllerMapper) + { + var model = cm.Key; + foreach(var controller in cm.Value) + { + var routeAttribute = controller.GetCustomAttribute(); + + controllerContexts.Add(new ControllerModelMap + { + Model = model, + Controller = controller, + Path = routeAttribute?.Template + }); + } + } + var graph = new ResourceGraph(_entities, _usesDbContext, _validationResults, controllerContexts); return graph; } @@ -169,9 +128,7 @@ protected virtual List GetAttributes(Type entityType) protected virtual List GetRelationships(Type entityType) { var attributes = new List(); - var properties = entityType.GetProperties(); - foreach (var prop in properties) { var attribute = (RelationshipAttribute)prop.GetCustomAttribute(typeof(RelationshipAttribute)); @@ -183,16 +140,17 @@ protected virtual List GetRelationships(Type entityType) attribute.PrincipalType = entityType; attributes.Add(attribute); - if (attribute is HasManyThroughAttribute hasManyThroughAttribute) { + if (attribute is HasManyThroughAttribute hasManyThroughAttribute) + { var throughProperty = properties.SingleOrDefault(p => p.Name == hasManyThroughAttribute.InternalThroughName); - if(throughProperty == null) + if (throughProperty == null) throw new JsonApiSetupException($"Invalid '{nameof(HasManyThroughAttribute)}' on type '{entityType}'. Type does not contain a property named '{hasManyThroughAttribute.InternalThroughName}'."); - - if(throughProperty.PropertyType.Implements() == false) + + if (throughProperty.PropertyType.Implements() == false) throw new JsonApiSetupException($"Invalid '{nameof(HasManyThroughAttribute)}' on type '{entityType}.{throughProperty.Name}'. Property type does not implement IList."); - + // assumption: the property should be a generic collection, e.g. List - if(throughProperty.PropertyType.IsGenericType == false) + if (throughProperty.PropertyType.IsGenericType == false) throw new JsonApiSetupException($"Invalid '{nameof(HasManyThroughAttribute)}' on type '{entityType}'. Expected through entity to be a generic type, such as List<{prop.PropertyType}>."); // Article → List @@ -202,7 +160,7 @@ protected virtual List GetRelationships(Type entityType) hasManyThroughAttribute.ThroughType = throughProperty.PropertyType.GetGenericArguments()[0]; var throughProperties = hasManyThroughAttribute.ThroughType.GetProperties(); - + // ArticleTag.Article hasManyThroughAttribute.LeftProperty = throughProperties.SingleOrDefault(x => x.PropertyType == entityType) ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {entityType}"); @@ -305,5 +263,24 @@ public IResourceGraphBuilder UseNameFormatter(IResourceNameFormatter resourceNam _resourceNameFormatter = resourceNameFormatter; return this; } + + public IResourceGraphBuilder AddControllerPairing(Type controller, Type model = null) + { + if (model == null) + { + _undefinedMapper.Add(controller); + return this; + + } + if (_controllerMapper.Keys.Contains(model)) + { + _controllerMapper[model].Add(controller); + } + else + { + _controllerMapper.Add(model, new List() { controller }); + } + return this; + } } } diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs new file mode 100644 index 0000000000..2ec70cc331 --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Text; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.Configuration +{ + public interface IJsonApiOptions + { + /// + /// Whether or not database values should be included by default + /// for resource hooks. Ignored if EnableResourceHooks is set false. + /// + /// Defaults to . + /// + bool LoadDatabaseValues { get; set; } + /// + /// Whether or not the total-record count should be included in all document + /// level meta objects. + /// Defaults to false. + /// + /// + /// options.IncludeTotalRecordCount = true; + /// + bool IncludeTotalRecordCount { get; set; } + int DefaultPageSize { get; } + bool ValidateModelState { get; } + bool AllowClientGeneratedIds { get; } + JsonSerializerSettings SerializerSettings { get; } + bool EnableOperations { get; set; } + Link DefaultRelationshipLinks { get; set; } + NullAttributeResponseBehavior NullAttributeResponseBehavior { get; set; } + bool RelativeLinks { get; set; } + IResourceGraph ResourceGraph { get; set; } + bool AllowCustomQueryParameters { get; set; } + string Namespace { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 21efdb97ed..456ff26263 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -3,6 +3,7 @@ using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; using Microsoft.EntityFrameworkCore; @@ -14,7 +15,7 @@ namespace JsonApiDotNetCore.Configuration /// /// Global options /// - public class JsonApiOptions + public class JsonApiOptions : IJsonApiOptions { /// @@ -92,6 +93,7 @@ public class JsonApiOptions /// /// The graph of all resources exposed by this application. /// + [Obsolete("Use the standalone resourcegraph")] public IResourceGraph ResourceGraph { get; set; } /// diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index 93fd4826e2..3a076430d7 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -1,41 +1,17 @@ +using System; using System.Collections.Generic; using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Controllers { - public class BaseJsonApiController - : BaseJsonApiController - where T : class, IIdentifiable - { - public BaseJsonApiController( - IJsonApiContext jsonApiContext, - IResourceService resourceService - ) : base(jsonApiContext, resourceService) { } - - public BaseJsonApiController( - IJsonApiContext jsonApiContext, - IResourceQueryService queryService = null, - IResourceCmdService cmdService = null - ) : base(jsonApiContext, queryService, cmdService) { } - - public BaseJsonApiController( - IJsonApiContext jsonApiContext, - IGetAllService getAll = null, - IGetByIdService getById = null, - IGetRelationshipService getRelationship = null, - IGetRelationshipsService getRelationships = null, - ICreateService create = null, - IUpdateService update = null, - IUpdateRelationshipService updateRelationships = null, - IDeleteService delete = null - ) : base(jsonApiContext, getAll, getById, getRelationship, getRelationships, create, update, updateRelationships, delete) { } - } - public class BaseJsonApiController : JsonApiControllerMixin where T : class, IIdentifiable @@ -48,13 +24,26 @@ public class BaseJsonApiController private readonly IUpdateService _update; private readonly IUpdateRelationshipService _updateRelationships; private readonly IDeleteService _delete; - private readonly IJsonApiContext _jsonApiContext; + private readonly ILogger> _logger; + private readonly IJsonApiOptions _jsonApiOptions; + private readonly IResourceGraph _resourceGraph; public BaseJsonApiController( - IJsonApiContext jsonApiContext, - IResourceService resourceService) + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraphManager, + IResourceService resourceService, + ILoggerFactory loggerFactory) { - _jsonApiContext = jsonApiContext.ApplyContext(this); + if(loggerFactory != null) + { + _logger = loggerFactory.CreateLogger>(); + } + else + { + _logger = new Logger>(new LoggerFactory()); + } + _jsonApiOptions = jsonApiOptions; + _resourceGraph = resourceGraphManager; _getAll = resourceService; _getById = resourceService; _getRelationship = resourceService; @@ -63,14 +52,20 @@ public BaseJsonApiController( _update = resourceService; _updateRelationships = resourceService; _delete = resourceService; + ParseQueryParams(); + } + + private void ParseQueryParams() + { + } public BaseJsonApiController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, IResourceQueryService queryService = null, IResourceCmdService cmdService = null) { - _jsonApiContext = jsonApiContext.ApplyContext(this); + _jsonApiOptions = jsonApiOptions; _getAll = queryService; _getById = queryService; _getRelationship = queryService; @@ -81,8 +76,22 @@ public BaseJsonApiController( _delete = cmdService; } + /// + /// Base constructor + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// public BaseJsonApiController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IGetAllService getAll = null, IGetByIdService getById = null, IGetRelationshipService getRelationship = null, @@ -92,7 +101,8 @@ public BaseJsonApiController( IUpdateRelationshipService updateRelationships = null, IDeleteService delete = null) { - _jsonApiContext = jsonApiContext.ApplyContext(this); + _resourceGraph = resourceGraph; + _jsonApiOptions = jsonApiOptions; _getAll = getAll; _getById = getById; _getRelationship = getRelationship; @@ -106,28 +116,27 @@ public BaseJsonApiController( public virtual async Task GetAsync() { if (_getAll == null) throw Exceptions.UnSupportedRequestMethod; - var entities = await _getAll.GetAsync(); - return Ok(entities); } public virtual async Task GetAsync(TId id) { if (_getById == null) throw Exceptions.UnSupportedRequestMethod; - var entity = await _getById.GetAsync(id); - if (entity == null) + { return NotFound(); - + } return Ok(entity); } public virtual async Task GetRelationshipsAsync(TId id, string relationshipName) { - if (_getRelationships == null) throw Exceptions.UnSupportedRequestMethod; - + if (_getRelationships == null) + { + throw Exceptions.UnSupportedRequestMethod; + } var relationship = await _getRelationships.GetRelationshipsAsync(id, relationshipName); if (relationship == null) return NotFound(); @@ -138,9 +147,7 @@ public virtual async Task GetRelationshipsAsync(TId id, string re public virtual async Task GetRelationshipAsync(TId id, string relationshipName) { if (_getRelationship == null) throw Exceptions.UnSupportedRequestMethod; - var relationship = await _getRelationship.GetRelationshipAsync(id, relationshipName); - return Ok(relationship); } @@ -152,11 +159,13 @@ public virtual async Task PostAsync([FromBody] T entity) if (entity == null) return UnprocessableEntity(); - if (!_jsonApiContext.Options.AllowClientGeneratedIds && !string.IsNullOrEmpty(entity.StringId)) + if (!_jsonApiOptions.AllowClientGeneratedIds && !string.IsNullOrEmpty(entity.StringId)) return Forbidden(); - if (_jsonApiContext.Options.ValidateModelState && !ModelState.IsValid) - return UnprocessableEntity(ModelState.ConvertToErrorCollection(_jsonApiContext.ResourceGraph)); + if (_jsonApiOptions.ValidateModelState && !ModelState.IsValid) + { + return UnprocessableEntity(ModelStateExtensions.ConvertToErrorCollection(ModelState, _resourceGraph)); + } entity = await _create.CreateAsync(entity); @@ -166,12 +175,13 @@ public virtual async Task PostAsync([FromBody] T entity) public virtual async Task PatchAsync(TId id, [FromBody] T entity) { if (_update == null) throw Exceptions.UnSupportedRequestMethod; - if (entity == null) return UnprocessableEntity(); - if (_jsonApiContext.Options.ValidateModelState && !ModelState.IsValid) - return UnprocessableEntity(ModelState.ConvertToErrorCollection(_jsonApiContext.ResourceGraph)); + if (_jsonApiOptions.ValidateModelState && !ModelState.IsValid) + { + return UnprocessableEntity(ModelStateExtensions.ConvertToErrorCollection(ModelState, _resourceGraph)); + } var updatedEntity = await _update.UpdateAsync(id, entity); @@ -184,22 +194,46 @@ public virtual async Task PatchAsync(TId id, [FromBody] T entity) public virtual async Task PatchRelationshipsAsync(TId id, string relationshipName, [FromBody] List relationships) { if (_updateRelationships == null) throw Exceptions.UnSupportedRequestMethod; - await _updateRelationships.UpdateRelationshipsAsync(id, relationshipName, relationships); - return Ok(); } public virtual async Task DeleteAsync(TId id) { if (_delete == null) throw Exceptions.UnSupportedRequestMethod; - var wasDeleted = await _delete.DeleteAsync(id); - if (!wasDeleted) return NotFound(); - return NoContent(); } } + public class BaseJsonApiController + : BaseJsonApiController + where T : class, IIdentifiable + { + public BaseJsonApiController( + IJsonApiOptions jsonApiOptions, + IResourceService resourceService + ) : base(jsonApiOptions, resourceService, resourceService) { } + + public BaseJsonApiController( + IJsonApiOptions jsonApiOptions, + IResourceQueryService queryService = null, + IResourceCmdService cmdService = null + ) : base(jsonApiOptions, queryService, cmdService) { } + + + public BaseJsonApiController( + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, + IGetAllService getAll = null, + IGetByIdService getById = null, + IGetRelationshipService getRelationship = null, + IGetRelationshipsService getRelationships = null, + ICreateService create = null, + IUpdateService update = null, + IUpdateRelationshipService updateRelationships = null, + IDeleteService delete = null + ) : base(jsonApiOptions, resourceGraph, getAll, getById, getRelationship, getRelationships, create, update, updateRelationships, delete) { } + } } diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs index 16ab4aa74a..3ee26db3c6 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs @@ -1,18 +1,21 @@ using System.Collections.Generic; using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc; namespace JsonApiDotNetCore.Controllers { - public class JsonApiCmdController - : JsonApiCmdController where T : class, IIdentifiable + public class JsonApiCmdController : JsonApiCmdController + where T : class, IIdentifiable { public JsonApiCmdController( + IJsonApiOptions jsonApiOptions, IJsonApiContext jsonApiContext, IResourceService resourceService) - : base(jsonApiContext, resourceService) + : base(jsonApiOptions, + jsonApiContext, resourceService) { } } @@ -20,9 +23,10 @@ public class JsonApiCmdController : BaseJsonApiController where T : class, IIdentifiable { public JsonApiCmdController( + IJsonApiOptions jsonApiOptions, IJsonApiContext jsonApiContext, IResourceService resourceService) - : base(jsonApiContext, resourceService) + : base(jsonApiOptions, resourceService) { } [HttpPost] diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs index a77c03da06..3ab5375db8 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs @@ -1,5 +1,8 @@ +using System; using System.Collections.Generic; using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc; @@ -7,53 +10,28 @@ namespace JsonApiDotNetCore.Controllers { - public class JsonApiController - : JsonApiController where T : class, IIdentifiable - { - public JsonApiController( - IJsonApiContext jsonApiContext, - IResourceService resourceService, - ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) - { } - public JsonApiController( - IJsonApiContext jsonApiContext, - IResourceService resourceService) - : base(jsonApiContext, resourceService) - { } - public JsonApiController( - IJsonApiContext jsonApiContext, - IGetAllService getAll = null, - IGetByIdService getById = null, - IGetRelationshipService getRelationship = null, - IGetRelationshipsService getRelationships = null, - ICreateService create = null, - IUpdateService update = null, - IUpdateRelationshipService updateRelationships = null, - IDeleteService delete = null - ) : base(jsonApiContext, getAll, getById, getRelationship, getRelationships, create, update, updateRelationships, delete) { } - } - - public class JsonApiController - : BaseJsonApiController where T : class, IIdentifiable + public class JsonApiController : BaseJsonApiController where T : class, IIdentifiable { + /// + /// + /// + /// + /// + /// + /// public JsonApiController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IResourceService resourceService, - ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService) + ILoggerFactory loggerFactory = null) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory = null) { } public JsonApiController( - IJsonApiContext jsonApiContext, - IResourceService resourceService) - : base(jsonApiContext, resourceService) - { } - - public JsonApiController( - IJsonApiContext jsonApiContext, + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, IGetAllService getAll = null, IGetByIdService getById = null, IGetRelationshipService getRelationship = null, @@ -62,7 +40,7 @@ public JsonApiController( IUpdateService update = null, IUpdateRelationshipService updateRelationships = null, IDeleteService delete = null - ) : base(jsonApiContext, getAll, getById, getRelationship, getRelationships, create, update, updateRelationships, delete) { } + ) : base(jsonApiOptions, resourceGraph, getAll, getById, getRelationship, getRelationships, create, update, updateRelationships, delete) { } [HttpGet] public override async Task GetAsync() => await base.GetAsync(); @@ -84,7 +62,9 @@ public override async Task PostAsync([FromBody] T entity) [HttpPatch("{id}")] public override async Task PatchAsync(TId id, [FromBody] T entity) - => await base.PatchAsync(id, entity); + { + return await base.PatchAsync(id, entity); + } [HttpPatch("{id}/relationships/{relationshipName}")] public override async Task PatchRelationshipsAsync( @@ -94,4 +74,59 @@ public override async Task PatchRelationshipsAsync( [HttpDelete("{id}")] public override async Task DeleteAsync(TId id) => await base.DeleteAsync(id); } + + /// + /// JsonApiController with int as default + /// + /// + public class JsonApiController : JsonApiController where T : class, IIdentifiable + { + private IJsonApiOptions jsonApiOptions; + private IJsonApiContext jsonApiContext; + private IResourceService resourceService; + private ILoggerFactory loggerFactory; + + + /// + /// Normal constructor with int as default (old fashioned) + /// + /// + /// + [Obsolete("JsonApiContext is Obsolete, use constructor without jsonApiContext")] + public JsonApiController( + IJsonApiContext context, + IResourceService resourceService) : base(context.Options,context.ResourceGraph,resourceService) { + } + + /// + /// Base constructor with int as default + /// + /// + /// + /// + /// + public JsonApiController( + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, + IResourceService resourceService, + ILoggerFactory loggerFactory = null + ) + : base(jsonApiOptions, resourceGraph, resourceService, loggerFactory) + { } + + public JsonApiController( + IJsonApiOptions jsonApiOptions, + IResourceGraph resourceGraph, + IGetAllService getAll = null, + IGetByIdService getById = null, + IGetRelationshipService getRelationship = null, + IGetRelationshipsService getRelationships = null, + ICreateService create = null, + IUpdateService update = null, + IUpdateRelationshipService updateRelationships = null, + IDeleteService delete = null + ) : base(jsonApiOptions, resourceGraph, getAll, getById, getRelationship, getRelationships, create, update, updateRelationships, delete) { } + + + } } diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs index 5211e5fa3b..21ca22a578 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc; @@ -9,9 +10,10 @@ public class JsonApiQueryController : JsonApiQueryController where T : class, IIdentifiable { public JsonApiQueryController( + IJsonApiOptions jsonApiOptions, IJsonApiContext jsonApiContext, IResourceService resourceService) - : base(jsonApiContext, resourceService) + : base(jsonApiOptions, jsonApiContext, resourceService) { } } @@ -19,9 +21,10 @@ public class JsonApiQueryController : BaseJsonApiController where T : class, IIdentifiable { public JsonApiQueryController( + IJsonApiOptions jsonApiOptions, IJsonApiContext jsonApiContext, IResourceService resourceService) - : base(jsonApiContext, resourceService) + : base(jsonApiOptions, resourceService) { } [HttpGet] diff --git a/src/JsonApiDotNetCore/Controllers/QueryParams.cs b/src/JsonApiDotNetCore/Controllers/QueryParams.cs index 4c963098ad..6e5e3901fb 100644 --- a/src/JsonApiDotNetCore/Controllers/QueryParams.cs +++ b/src/JsonApiDotNetCore/Controllers/QueryParams.cs @@ -2,7 +2,7 @@ namespace JsonApiDotNetCore.Controllers { public enum QueryParams { - Filter = 1 << 0, + Filters = 1 << 0, Sort = 1 << 1, Include = 1 << 2, Page = 1 << 3, diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 889e2f7198..00169edd0a 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -7,33 +7,14 @@ using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Data { - /// - public class DefaultEntityRepository - : DefaultEntityRepository, - IEntityRepository - where TEntity : class, IIdentifiable - { - public DefaultEntityRepository( - IJsonApiContext jsonApiContext, - IDbContextResolver contextResolver, - ResourceDefinition resourceDefinition = null) - : base(jsonApiContext, contextResolver, resourceDefinition) - { } - public DefaultEntityRepository( - ILoggerFactory loggerFactory, - IJsonApiContext jsonApiContext, - IDbContextResolver contextResolver, - ResourceDefinition resourceDefinition = null) - : base(loggerFactory, jsonApiContext, contextResolver, resourceDefinition) - { } - } /// /// Provides a default repository implementation and is responsible for @@ -44,17 +25,24 @@ public class DefaultEntityRepository IEntityFrameworkRepository where TEntity : class, IIdentifiable { + private readonly IRequestManager _requestManager; private readonly DbContext _context; private readonly DbSet _dbSet; private readonly ILogger _logger; private readonly IJsonApiContext _jsonApiContext; private readonly IGenericProcessorFactory _genericProcessorFactory; private readonly ResourceDefinition _resourceDefinition; + + + + + [Obsolete("Dont use jsonapicontext instantiation anymore")] public DefaultEntityRepository( IJsonApiContext jsonApiContext, IDbContextResolver contextResolver, ResourceDefinition resourceDefinition = null) { + _requestManager = jsonApiContext.RequestManager; _context = contextResolver.GetContext(); _dbSet = _context.Set(); _jsonApiContext = jsonApiContext; @@ -62,12 +50,15 @@ public DefaultEntityRepository( _resourceDefinition = resourceDefinition; } + [Obsolete("Dont use jsonapicontext instantiation anymore")] public DefaultEntityRepository( ILoggerFactory loggerFactory, IJsonApiContext jsonApiContext, IDbContextResolver contextResolver, ResourceDefinition resourceDefinition = null) { + _requestManager = jsonApiContext.RequestManager; + _context = contextResolver.GetContext(); _dbSet = _context.Set(); _jsonApiContext = jsonApiContext; @@ -77,8 +68,7 @@ public DefaultEntityRepository( } /// - public virtual IQueryable Get() - => _dbSet; + public virtual IQueryable Get() => _dbSet; /// public virtual IQueryable Select(IQueryable entities, List fields) @@ -100,7 +90,7 @@ public virtual IQueryable Filter(IQueryable entities, FilterQu return defaultQueryFilter(entities, filterQuery); } } - return entities.Filter(_jsonApiContext, filterQuery); + return entities; } /// @@ -127,24 +117,22 @@ public virtual IQueryable Sort(IQueryable entities, List public virtual async Task GetAsync(TId id) { - return await Select(Get(), _jsonApiContext.QuerySet?.Fields).SingleOrDefaultAsync(e => e.Id.Equals(id)); + return await Select(Get(), _requestManager.QuerySet?.Fields).SingleOrDefaultAsync(e => e.Id.Equals(id)); } /// public virtual async Task GetAndIncludeAsync(TId id, string relationshipName) { _logger?.LogDebug($"[JADN] GetAndIncludeAsync({id}, {relationshipName})"); - - var includedSet = Include(Select(Get(), _jsonApiContext.QuerySet?.Fields), relationshipName); + var includedSet = Include(Select(Get(), _requestManager.QuerySet?.Fields), relationshipName); var result = await includedSet.SingleOrDefaultAsync(e => e.Id.Equals(id)); - return result; } /// public virtual async Task CreateAsync(TEntity entity) { - foreach (var relationshipAttr in _jsonApiContext.RelationshipsToUpdate?.Keys) + foreach (var relationshipAttr in _requestManager.GetUpdatedRelationships()?.Keys) { var trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, entity, out bool wasAlreadyTracked); LoadInverseRelationships(trackedRelationshipValue, relationshipAttr); @@ -208,11 +196,12 @@ private void LoadInverseRelationships(object trackedRelationshipValue, Relations private bool IsHasOneRelationship(string internalRelationshipName, Type type) { var relationshipAttr = _jsonApiContext.ResourceGraph.GetContextEntity(type).Relationships.SingleOrDefault(r => r.InternalRelationshipName == internalRelationshipName); - if(relationshipAttr != null) + if (relationshipAttr != null) { if (relationshipAttr is HasOneAttribute) return true; return false; - } else + } + else { // relationshipAttr is null when we don't put a [RelationshipAttribute] on the inverse navigation property. // In this case we use relfection to figure out what kind of relationship is pointing back. @@ -225,7 +214,7 @@ private bool IsHasOneRelationship(string internalRelationshipName, Type type) public void DetachRelationshipPointers(TEntity entity) { - foreach (var relationshipAttr in _jsonApiContext.RelationshipsToUpdate.Keys) + foreach (var relationshipAttr in _requestManager.GetUpdatedRelationships().Keys) { if (relationshipAttr is HasOneAttribute hasOneAttr) { @@ -268,10 +257,10 @@ public virtual async Task UpdateAsync(TEntity updatedEntity) if (databaseEntity == null) return null; - foreach (var attr in _jsonApiContext.AttributesToUpdate.Keys) + foreach (var attr in _requestManager.GetUpdatedAttributes().Keys) attr.SetValue(databaseEntity, attr.GetValue(updatedEntity)); - foreach (var relationshipAttr in _jsonApiContext.RelationshipsToUpdate?.Keys) + foreach (var relationshipAttr in _requestManager.GetUpdatedRelationships()?.Keys) { /// loads databasePerson.todoItems LoadCurrentRelationships(databaseEntity, relationshipAttr); @@ -330,7 +319,7 @@ private IList GetTrackedManyRelationshipValue(IEnumerable relatio /// from the RelationshipAttribute. If we DO use separation, RelationshipAttribute.DependentType /// will point to the Resource, not the Entity, which is not the one we need here. bool entityResourceSeparation = relationshipAttr.EntityPropertyName != null; - Type entityType = entityResourceSeparation ? null : relationshipAttr.DependentType; + Type entityType = entityResourceSeparation ? null : relationshipAttr.DependentType; var trackedPointerCollection = relationshipValueList.Select(pointer => { /// todo: we can't just use relationshipAttr.DependentType because @@ -390,7 +379,7 @@ public virtual IQueryable Include(IQueryable entities, string // variables mutated in recursive loop // TODO: make recursive method string internalRelationshipPath = null; - var entity = _jsonApiContext.RequestEntity; + var entity = _requestManager.GetContextEntity(); for (var i = 0; i < relationshipChain.Length; i++) { var requestedRelationship = relationshipChain[i]; @@ -433,7 +422,7 @@ public virtual async Task> PageAsync(IQueryable en // may be negative int virtualFirstIndex = numberOfEntities - pageSize * Math.Abs(pageNumber); int numberOfElementsInPage = Math.Min(pageSize, virtualFirstIndex + pageSize); - + return await ToListAsync(entities .Skip(virtualFirstIndex) .Take(numberOfElementsInPage)); @@ -581,4 +570,25 @@ private IIdentifiable AttachOrGetTracked(IIdentifiable relationshipValue) return null; } } + /// + public class DefaultEntityRepository + : DefaultEntityRepository, + IEntityRepository + where TEntity : class, IIdentifiable + { + public DefaultEntityRepository( + IJsonApiContext jsonApiContext, + IDbContextResolver contextResolver, + ResourceDefinition resourceDefinition = null) + : base(jsonApiContext, contextResolver, resourceDefinition) + { } + + public DefaultEntityRepository( + ILoggerFactory loggerFactory, + IJsonApiContext jsonApiContext, + IDbContextResolver contextResolver, + ResourceDefinition resourceDefinition = null) + : base(loggerFactory, jsonApiContext, contextResolver, resourceDefinition) + { } + } } diff --git a/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs b/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs index e66b51a7a0..91f7baf180 100644 --- a/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs @@ -1,6 +1,7 @@ using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Middleware; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 87de9d187d..b6b41fd0ca 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -113,17 +113,7 @@ private static IOrderedQueryable CallGenericOrderMethod(IQuery return (IOrderedQueryable)result; } - public static IQueryable Filter(this IQueryable source, IJsonApiContext jsonApiContext, FilterQuery filterQuery) - { - if (filterQuery == null) - return source; - // Relationship.Attribute - if (filterQuery.IsAttributeOfRelationship) - return source.Filter(new RelatedAttrFilterQuery(jsonApiContext, filterQuery)); - - return source.Filter(new AttrFilterQuery(jsonApiContext, filterQuery)); - } public static IQueryable Filter(this IQueryable source, BaseFilterQuery filterQuery) { diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 6c3779cbb3..a37d713db7 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -9,6 +9,8 @@ using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Generics; +using JsonApiDotNetCore.Managers; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; @@ -48,9 +50,7 @@ public static IServiceCollection AddJsonApi( var config = new JsonApiOptions(); options(config); config.BuildResourceGraph(builder => builder.AddDbContext()); - mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, config)); - AddJsonApiInternals(services, config); return services; } @@ -63,15 +63,12 @@ public static IServiceCollection AddJsonApi( { var config = new JsonApiOptions(); configureOptions(config); - - if(autoDiscover != null) + if (autoDiscover != null) { var facade = new ServiceDiscoveryFacade(services, config.ResourceGraphBuilder); autoDiscover(facade); } - mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, config)); - AddJsonApiInternals(services, config); return services; } @@ -100,7 +97,9 @@ public static void AddJsonApiInternals( JsonApiOptions jsonApiOptions) { if (jsonApiOptions.ResourceGraph == null) + { jsonApiOptions.ResourceGraph = jsonApiOptions.ResourceGraphBuilder.Build(); + } if (jsonApiOptions.ResourceGraph.UsesDbContext == false) { @@ -109,7 +108,9 @@ public static void AddJsonApiInternals( } if (jsonApiOptions.EnableOperations) + { AddOperationServices(services); + } services.AddScoped(typeof(IEntityRepository<>), typeof(DefaultEntityRepository<>)); services.AddScoped(typeof(IEntityRepository<,>), typeof(DefaultEntityRepository<,>)); @@ -117,8 +118,6 @@ public static void AddJsonApiInternals( services.AddScoped(typeof(IEntityReadRepository<,>), typeof(DefaultEntityRepository<,>)); services.AddScoped(typeof(IEntityWriteRepository<,>), typeof(DefaultEntityRepository<,>)); - - services.AddScoped(typeof(ICreateService<>), typeof(EntityResourceService<>)); services.AddScoped(typeof(ICreateService<,>), typeof(EntityResourceService<,>)); @@ -140,7 +139,11 @@ public static void AddJsonApiInternals( services.AddScoped(typeof(IResourceService<>), typeof(EntityResourceService<>)); services.AddScoped(typeof(IResourceService<,>), typeof(EntityResourceService<,>)); - services.AddSingleton(jsonApiOptions); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddSingleton(jsonApiOptions); + services.AddScoped(); services.AddSingleton(jsonApiOptions.ResourceGraph); services.AddScoped(); services.AddSingleton(); @@ -169,7 +172,6 @@ public static void AddJsonApiInternals( } services.AddScoped(); - } private static void AddOperationServices(IServiceCollection services) @@ -194,9 +196,7 @@ private static void AddOperationServices(IServiceCollection services) public static void SerializeAsJsonApi(this MvcOptions options, JsonApiOptions jsonApiOptions) { options.InputFormatters.Insert(0, new JsonApiInputFormatter()); - options.OutputFormatters.Insert(0, new JsonApiOutputFormatter()); - options.Conventions.Insert(0, new DasherizedRoutingConvention(jsonApiOptions.Namespace)); } @@ -204,7 +204,7 @@ public static void SerializeAsJsonApi(this MvcOptions options, JsonApiOptions js /// Adds all required registrations for the service to the container /// /// - public static IServiceCollection AddResourceService(this IServiceCollection services) + public static IServiceCollection AddResourceService(this IServiceCollection services) { var typeImplementsAnExpectedInterface = false; @@ -213,28 +213,29 @@ public static IServiceCollection AddResourceService(this IServiceCollection s // it is _possible_ that a single concrete type could be used for multiple resources... var resourceDescriptors = GetResourceTypesFromServiceImplementation(serviceImplementationType); - foreach(var resourceDescriptor in resourceDescriptors) + foreach (var resourceDescriptor in resourceDescriptors) { - foreach(var openGenericType in ServiceDiscoveryFacade.ServiceInterfaces) + foreach (var openGenericType in ServiceDiscoveryFacade.ServiceInterfaces) { // A shorthand interface is one where the id type is ommitted // e.g. IResourceService is the shorthand for IResourceService var isShorthandInterface = (openGenericType.GetTypeInfo().GenericTypeParameters.Length == 1); - if(isShorthandInterface && resourceDescriptor.IdType != typeof(int)) + if (isShorthandInterface && resourceDescriptor.IdType != typeof(int)) continue; // we can't create a shorthand for id types other than int var concreteGenericType = isShorthandInterface ? openGenericType.MakeGenericType(resourceDescriptor.ResourceType) : openGenericType.MakeGenericType(resourceDescriptor.ResourceType, resourceDescriptor.IdType); - if(concreteGenericType.IsAssignableFrom(serviceImplementationType)) { + if (concreteGenericType.IsAssignableFrom(serviceImplementationType)) + { services.AddScoped(concreteGenericType, serviceImplementationType); typeImplementsAnExpectedInterface = true; } } } - if(typeImplementsAnExpectedInterface == false) + if (typeImplementsAnExpectedInterface == false) throw new JsonApiSetupException($"{serviceImplementationType} does not implement any of the expected JsonApiDotNetCore interfaces."); return services; @@ -244,18 +245,17 @@ private static HashSet GetResourceTypesFromServiceImplementa { var resourceDecriptors = new HashSet(); var interfaces = type.GetInterfaces(); - foreach(var i in interfaces) + foreach (var i in interfaces) { - if(i.IsGenericType) + if (i.IsGenericType) { var firstGenericArgument = i.GenericTypeArguments.FirstOrDefault(); - if(TypeLocator.TryGetResourceDescriptor(firstGenericArgument, out var resourceDescriptor) == true) + if (TypeLocator.TryGetResourceDescriptor(firstGenericArgument, out var resourceDescriptor) == true) { resourceDecriptors.Add(resourceDescriptor); } } } - return resourceDecriptors; } } diff --git a/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs b/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs index 023ef09ae2..587f3749f3 100644 --- a/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs @@ -1,5 +1,6 @@ using System; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.EntityFrameworkCore.Internal; @@ -29,10 +30,9 @@ public static ErrorCollection ConvertToErrorCollection(this ModelStateDiction meta: modelError.Exception != null ? ErrorMeta.FromException(modelError.Exception) : null, source: attrName == null ? null : new { pointer = $"/data/attributes/{attrName}" - })); + })); } } - return collection; } } diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs b/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs index b305bf9722..3727a9a750 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs @@ -15,13 +15,13 @@ namespace JsonApiDotNetCore.Formatters /// public class JsonApiReader : IJsonApiReader { - private readonly IJsonApiDeSerializer _deSerializer; + private readonly IJsonApiDeSerializer _deserializer; private readonly IJsonApiContext _jsonApiContext; private readonly ILogger _logger; public JsonApiReader(IJsonApiDeSerializer deSerializer, IJsonApiContext jsonApiContext, ILoggerFactory loggerFactory) { - _deSerializer = deSerializer; + _deserializer = deSerializer; _jsonApiContext = jsonApiContext; _logger = loggerFactory.CreateLogger(); } @@ -40,17 +40,14 @@ public Task ReadAsync(InputFormatterContext context) var body = GetRequestBody(context.HttpContext.Request.Body); object model = null; - if (_jsonApiContext.IsRelationshipPath) { - model = _deSerializer.DeserializeRelationship(body); + model = _deserializer.DeserializeRelationship(body); } else { - model = _deSerializer.Deserialize(body); + model = _deserializer.Deserialize(body); } - - if (model == null) { _logger?.LogError("An error occurred while de-serializing the payload"); diff --git a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs index 99c3845108..d75cf9818e 100644 --- a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs @@ -1,9 +1,12 @@ using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Data; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using System; @@ -17,7 +20,7 @@ public class ServiceDiscoveryFacade { internal static HashSet ServiceInterfaces = new HashSet { typeof(IResourceService<>), - typeof(IResourceService<,>), + typeof(IResourceService<,>), typeof(IResourceCmdService<>), typeof(IResourceCmdService<,>), typeof(IResourceQueryService<>), @@ -46,13 +49,12 @@ public class ServiceDiscoveryFacade typeof(IEntityReadRepository<>), typeof(IEntityReadRepository<,>) }; - private readonly IServiceCollection _services; private readonly IResourceGraphBuilder _graphBuilder; private readonly List _identifiables = new List(); public ServiceDiscoveryFacade( - IServiceCollection services, + IServiceCollection services, IResourceGraphBuilder graphBuilder) { _services = services; @@ -80,13 +82,56 @@ public ServiceDiscoveryFacade AddAssembly(Assembly assembly) AddRepositories(assembly, resourceDescriptor); } + ScanControllers(assembly); + return this; } + private void ScanControllers(Assembly assembly) + { + var baseTypes = new List() { typeof(ControllerBase), typeof(JsonApiControllerMixin), typeof(JsonApiController<>), typeof(BaseJsonApiController<>) }; + List baseTypesSeen = new List() { }; + var types = assembly.GetTypes().ToList(); + //types.ForEach(t => baseTypesSeen.Add(t.BaseType.Name)); + + var controllerMapper = new Dictionary>() { }; + var undefinedMapper = new List() { }; + var sdf = assembly.GetTypes() + .Where(type => typeof(ControllerBase).IsAssignableFrom(type) & !type.IsGenericType).ToList(); + foreach (var controllerType in sdf) + { + // get generic parameter + var genericParameters = controllerType.BaseType.GetGenericArguments(); + if (genericParameters.Count() > 0) + { + + _graphBuilder.AddControllerPairing(controllerType, genericParameters[0]); + } + else + { + _graphBuilder.AddControllerPairing(controllerType); + } + } + } + + public IEnumerable FindDerivedTypes(Type baseType) + { + return baseType.Assembly.GetTypes().Where(t => + { + if (t.BaseType != null) + { + return baseType.IsSubclassOf(t); + } + return false; + + }); + } + + private void AddDbContextResolvers(Assembly assembly) { var dbContextTypes = TypeLocator.GetDerivedTypes(assembly, typeof(DbContext)); - foreach(var dbContextType in dbContextTypes) + foreach (var dbContextType in dbContextTypes) { var resolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); _services.AddScoped(typeof(IDbContextResolver), resolverType); @@ -125,7 +170,7 @@ private void RegisterResourceDefinition(Assembly assembly, ResourceDescriptor id catch (InvalidOperationException e) { throw new JsonApiSetupException($"Cannot define multiple ResourceDefinition<> implementations for '{identifiable.ResourceType}'", e); - } + } } private void AddResourceToGraph(ResourceDescriptor identifiable) @@ -134,7 +179,7 @@ private void AddResourceToGraph(ResourceDescriptor identifiable) _graphBuilder.AddResource(identifiable.ResourceType, identifiable.IdType, resourceName); } - private string FormatResourceName(Type resourceType) + private string FormatResourceName(Type resourceType) => JsonApiOptions.ResourceNameFormatter.FormatResourceName(resourceType); /// @@ -145,45 +190,59 @@ public ServiceDiscoveryFacade AddServices(Assembly assembly) { var resourceDescriptors = TypeLocator.GetIdentifableTypes(assembly); foreach (var resourceDescriptor in resourceDescriptors) + { AddServices(assembly, resourceDescriptor); - + } return this; } private void AddServices(Assembly assembly, ResourceDescriptor resourceDescriptor) { - foreach(var serviceInterface in ServiceInterfaces) + foreach (var serviceInterface in ServiceInterfaces) + { RegisterServiceImplementations(assembly, serviceInterface, resourceDescriptor); + } } /// /// Add implementations to container. /// /// The assembly to search for resources in. - public ServiceDiscoveryFacade AddRepositories(Assembly assembly) + public ServiceDiscoveryFacade AddRepositories(Assembly assembly) { var resourceDescriptors = TypeLocator.GetIdentifableTypes(assembly); foreach (var resourceDescriptor in resourceDescriptors) + { AddRepositories(assembly, resourceDescriptor); + } return this; } private void AddRepositories(Assembly assembly, ResourceDescriptor resourceDescriptor) { - foreach(var serviceInterface in RepositoryInterfaces) + foreach (var serviceInterface in RepositoryInterfaces) + { RegisterServiceImplementations(assembly, serviceInterface, resourceDescriptor); + } } - + public int i = 0; private void RegisterServiceImplementations(Assembly assembly, Type interfaceType, ResourceDescriptor resourceDescriptor) { - var genericArguments = interfaceType.GetTypeInfo().GenericTypeParameters.Length == 2 - ? new [] { resourceDescriptor.ResourceType, resourceDescriptor.IdType } - : new [] { resourceDescriptor.ResourceType }; - + if (resourceDescriptor.IdType == typeof(Guid) && interfaceType.GetTypeInfo().GenericTypeParameters.Length == 1) + { + return; + } + var genericArguments = interfaceType.GetTypeInfo().GenericTypeParameters.Length == 2 ? new[] { resourceDescriptor.ResourceType, resourceDescriptor.IdType } : new[] { resourceDescriptor.ResourceType }; var service = TypeLocator.GetGenericInterfaceImplementation(assembly, interfaceType, genericArguments); + //if(service.implementation?.Name == "CustomArticleService" && genericArguments[0].Name != "Article") + //{ + // service = TypeLocator.GetGenericInterfaceImplementation(assembly, interfaceType, genericArguments); + //} if (service.implementation != null) + { _services.AddScoped(service.registrationInterface, service.implementation); + } } } } diff --git a/src/JsonApiDotNetCore/Graph/TypeLocator.cs b/src/JsonApiDotNetCore/Graph/TypeLocator.cs index 610b813428..1e82e438c3 100644 --- a/src/JsonApiDotNetCore/Graph/TypeLocator.cs +++ b/src/JsonApiDotNetCore/Graph/TypeLocator.cs @@ -14,7 +14,7 @@ static class TypeLocator private static Dictionary _typeCache = new Dictionary(); private static Dictionary> _identifiableTypeCache = new Dictionary>(); - + /// /// Determine whether or not this is a json:api resource by checking if it implements . /// Returns the status and the resultant id type, either `(true, Type)` OR `(false, null)` @@ -48,10 +48,10 @@ private static Type[] GetAssemblyTypes(Assembly assembly) /// /// Get all implementations of in the assembly /// - public static IEnumerable GetIdentifableTypes(Assembly assembly) + public static IEnumerable GetIdentifableTypes(Assembly assembly) => (_identifiableTypeCache.TryGetValue(assembly, out var descriptors) == false) ? FindIdentifableTypes(assembly) - : _identifiableTypeCache[assembly]; + : _identifiableTypeCache[assembly]; private static IEnumerable FindIdentifableTypes(Assembly assembly) { @@ -60,7 +60,7 @@ private static IEnumerable FindIdentifableTypes(Assembly ass foreach (var type in assembly.GetTypes()) { - if (TryGetResourceDescriptor(type, out var descriptor)) + if (TryGetResourceDescriptor(type, out var descriptor)) { descriptors.Add(descriptor); yield return descriptor; @@ -77,15 +77,15 @@ private static IEnumerable FindIdentifableTypes(Assembly ass internal static bool TryGetResourceDescriptor(Type type, out ResourceDescriptor descriptor) { var possible = GetIdType(type); - if (possible.isJsonApiResource) { + if (possible.isJsonApiResource) + { descriptor = new ResourceDescriptor(type, possible.idType); return true; - } - + } + descriptor = ResourceDescriptor.Empty; return false; } - /// /// Get all implementations of the generic interface /// @@ -99,11 +99,11 @@ internal static bool TryGetResourceDescriptor(Type type, out ResourceDescriptor /// public static (Type implementation, Type registrationInterface) GetGenericInterfaceImplementation(Assembly assembly, Type openGenericInterfaceType, params Type[] genericInterfaceArguments) { - if(assembly == null) throw new ArgumentNullException(nameof(assembly)); - if(openGenericInterfaceType == null) throw new ArgumentNullException(nameof(openGenericInterfaceType)); - if(genericInterfaceArguments == null) throw new ArgumentNullException(nameof(genericInterfaceArguments)); - if(genericInterfaceArguments.Length == 0) throw new ArgumentException("No arguments supplied for the generic interface.", nameof(genericInterfaceArguments)); - if(openGenericInterfaceType.IsGenericType == false) throw new ArgumentException("Requested type is not a generic type.", nameof(openGenericInterfaceType)); + if (assembly == null) throw new ArgumentNullException(nameof(assembly)); + if (openGenericInterfaceType == null) throw new ArgumentNullException(nameof(openGenericInterfaceType)); + if (genericInterfaceArguments == null) throw new ArgumentNullException(nameof(genericInterfaceArguments)); + if (genericInterfaceArguments.Length == 0) throw new ArgumentException("No arguments supplied for the generic interface.", nameof(genericInterfaceArguments)); + if (openGenericInterfaceType.IsGenericType == false) throw new ArgumentException("Requested type is not a generic type.", nameof(openGenericInterfaceType)); foreach (var type in assembly.GetTypes()) { @@ -113,7 +113,8 @@ public static (Type implementation, Type registrationInterface) GetGenericInterf if (interfaceType.IsGenericType) { var genericTypeDefinition = interfaceType.GetGenericTypeDefinition(); - if(genericTypeDefinition == openGenericInterfaceType.GetGenericTypeDefinition()) { + if (interfaceType.GetGenericArguments().First() == genericInterfaceArguments.First() &&genericTypeDefinition == openGenericInterfaceType.GetGenericTypeDefinition()) + { return ( type, genericTypeDefinition.MakeGenericType(genericInterfaceArguments) @@ -157,7 +158,7 @@ public static IEnumerable GetDerivedTypes(Assembly assembly, Type inherite { foreach (var type in assembly.GetTypes()) { - if(inheritedType.IsAssignableFrom(type)) + if (inheritedType.IsAssignableFrom(type)) yield return type; } } diff --git a/src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs b/src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs index df7f4daa0c..4d3a80bc7b 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -12,28 +12,30 @@ using Microsoft.EntityFrameworkCore; using JsonApiDotNetCore.Services; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Configuration; namespace JsonApiDotNetCore.Hooks { /// internal class HookExecutorHelper : IHookExecutorHelper { + private readonly IJsonApiOptions _options; protected readonly IGenericProcessorFactory _genericProcessorFactory; protected readonly IResourceGraph _graph; protected readonly Dictionary _hookContainers; protected readonly Dictionary _hookDiscoveries; protected readonly List _targetedHooksForRelatedEntities; - protected readonly IJsonApiContext _context; public HookExecutorHelper( IGenericProcessorFactory genericProcessorFactory, IResourceGraph graph, - IJsonApiContext context + IJsonApiOptions options ) { + _options = options; _genericProcessorFactory = genericProcessorFactory; _graph = graph; - _context = context; _hookContainers = new Dictionary(); _hookDiscoveries = new Dictionary(); _targetedHooksForRelatedEntities = new List(); @@ -116,7 +118,7 @@ public bool ShouldLoadDbValues(Type entityType, ResourceHook hook) } else { - return _context.Options.LoadDatabaseValues; + return _options.LoadDatabaseValues; } } @@ -219,4 +221,4 @@ bool IsHasManyThrough(KeyValuePair kvp, return (kvp.Key is HasManyThroughAttribute); } } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs index 30bb51560d..808ed3bc90 100644 --- a/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs @@ -9,24 +9,27 @@ using DependentType = System.Type; using JsonApiDotNetCore.Services; using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; namespace JsonApiDotNetCore.Hooks { /// - internal class ResourceHookExecutor : IResourceHookExecutor + internal class ResourceHookExecutor : IResourceHookExecutor { public static readonly IdentifiableComparer Comparer = new IdentifiableComparer(); - internal readonly TraversalHelper _traversalHelper; + internal readonly ITraversalHelper _traversalHelper; + private readonly IRequestManager _requestManager; internal readonly IHookExecutorHelper _executorHelper; protected readonly IJsonApiContext _context; private readonly IResourceGraph _graph; - public ResourceHookExecutor(IHookExecutorHelper helper, IJsonApiContext context, IResourceGraph graph) + public ResourceHookExecutor(IHookExecutorHelper helper, ITraversalHelper traversalHelper, IResourceGraph resourceGraph, IRequestManager requestManager) { + _requestManager = requestManager; _executorHelper = helper; - _context = context; - _graph = graph; - _traversalHelper = new TraversalHelper(graph, _context); + _graph = resourceGraph; + _traversalHelper = traversalHelper; } /// @@ -36,7 +39,7 @@ public virtual void BeforeRead(ResourcePipeline pipeline, string string hookContainer?.BeforeRead(pipeline, false, stringId); var contextEntity = _graph.GetContextEntity(typeof(TEntity)); var calledContainers = new List() { typeof(TEntity) }; - foreach (var relationshipPath in _context.IncludedRelationships) + foreach (var relationshipPath in _requestManager.IncludedRelationships) { RecursiveBeforeRead(contextEntity, relationshipPath.Split('.').ToList(), pipeline, calledContainers); } @@ -187,12 +190,12 @@ bool GetHook(ResourceHook target, IEnumerable entities, } /// - /// Traverses the nodes in a . + /// Traverses the nodes in a . /// - void Traverse(EntityChildLayer currentLayer, ResourceHook target, Action action) + void Traverse(NodeLayer currentLayer, ResourceHook target, Action action) { if (!currentLayer.AnyEntities()) return; - foreach (IEntityNode node in currentLayer) + foreach (INode node in currentLayer) { var entityType = node.EntityType; var hookContainer = _executorHelper.GetResourceHookContainer(entityType, target); @@ -245,9 +248,9 @@ void RecursiveBeforeRead(ContextEntity contextEntity, List relationshipC /// First the BeforeUpdateRelationship should be for owner1, then the /// BeforeImplicitUpdateRelationship hook should be fired for /// owner2, and lastely the BeforeImplicitUpdateRelationship for article2. - void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, EntityChildLayer layer) + void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, NodeLayer layer) { - foreach (IEntityNode node in layer) + foreach (INode node in layer) { var nestedHookcontainer = _executorHelper.GetResourceHookContainer(node.EntityType, ResourceHook.BeforeUpdateRelationship); IEnumerable uniqueEntities = node.UniqueEntities; @@ -447,7 +450,7 @@ IEnumerable LoadDbValues(Type entityType, IEnumerable uniqueEntities, ResourceHo /// /// Fires the AfterUpdateRelationship hook /// - void FireAfterUpdateRelationship(IResourceHookContainer container, IEntityNode node, ResourcePipeline pipeline) + void FireAfterUpdateRelationship(IResourceHookContainer container, INode node, ResourcePipeline pipeline) { Dictionary currenEntitiesGrouped = node.RelationshipsFromPreviousLayer.GetDependentEntities(); diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/ChildNode.cs b/src/JsonApiDotNetCore/Hooks/Traversal/ChildNode.cs index 443ee7daf1..2fa1ae2aa2 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/ChildNode.cs +++ b/src/JsonApiDotNetCore/Hooks/Traversal/ChildNode.cs @@ -1,4 +1,4 @@ -using System.Collections; +using System.Collections; using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Extensions; @@ -7,8 +7,11 @@ namespace JsonApiDotNetCore.Hooks { - /// - internal class ChildNode : IEntityNode where TEntity : class, IIdentifiable + /// + /// Child node in the tree + /// + /// + internal class ChildNode : INode where TEntity : class, IIdentifiable { /// public DependentType EntityType { get; private set; } @@ -51,7 +54,10 @@ public void UpdateUnique(IEnumerable updated) } } - /// + /// + /// Reassignment is done according to provided relationships + /// + /// public void Reassign(IEnumerable updated = null) { var unique = (HashSet)UniqueEntities; diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/IEntityNode.cs b/src/JsonApiDotNetCore/Hooks/Traversal/IEntityNode.cs index 159b373ef5..00e8a0b8f3 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/IEntityNode.cs +++ b/src/JsonApiDotNetCore/Hooks/Traversal/IEntityNode.cs @@ -1,9 +1,12 @@ -using System.Collections; +using System.Collections; using DependentType = System.Type; namespace JsonApiDotNetCore.Hooks { - internal interface IEntityNode + /// + /// This is the interface that nodes need to inherit from + /// + internal interface INode { /// /// Each node representes the entities of a given type throughout a particular layer. diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/ITraversalHelper.cs b/src/JsonApiDotNetCore/Hooks/Traversal/ITraversalHelper.cs new file mode 100644 index 0000000000..f0449b9756 --- /dev/null +++ b/src/JsonApiDotNetCore/Hooks/Traversal/ITraversalHelper.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Hooks +{ + internal interface ITraversalHelper + { + /// + /// Crates the next layer + /// + /// + /// + NodeLayer CreateNextLayer(INode node); + /// + /// Creates the next layer based on the nodes provided + /// + /// + /// + NodeLayer CreateNextLayer(IEnumerable nodes); + /// + /// Creates a root node for breadth-first-traversal (BFS). Note that typically, in + /// JADNC, the root layer will be homogeneous. Also, because it is the first layer, + /// there can be no relationships to previous layers, only to next layers. + /// + /// The root node. + /// Root entities. + /// The 1st type parameter. + RootNode CreateRootNode(IEnumerable rootEntities) where TEntity : class, IIdentifiable; + } +} diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/RootNode.cs b/src/JsonApiDotNetCore/Hooks/Traversal/RootNode.cs index 7783d041e1..1aa3c0eb8b 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/RootNode.cs +++ b/src/JsonApiDotNetCore/Hooks/Traversal/RootNode.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -11,7 +11,7 @@ namespace JsonApiDotNetCore.Hooks /// The root node class of the breadth-first-traversal of entity data structures /// as performed by the /// - internal class RootNode : IEntityNode where TEntity : class, IIdentifiable + internal class RootNode : INode where TEntity : class, IIdentifiable { private readonly RelationshipProxy[] _allRelationshipsToNextLayer; private HashSet _uniqueEntities; diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs b/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs index 5061cce9bf..00c1c4dca8 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs @@ -1,10 +1,12 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using DependentType = System.Type; @@ -21,10 +23,11 @@ namespace JsonApiDotNetCore.Hooks /// and further nodes can be mixed. /// /// - internal class TraversalHelper + internal class TraversalHelper : ITraversalHelper { private readonly IResourceGraph _graph; - private readonly IJsonApiContext _context; + private readonly IRequestManager _requestManager; + /// /// Keeps track of which entities has already been traversed through, to prevent /// infinite loops in eg cyclic data structures. @@ -35,12 +38,11 @@ internal class TraversalHelper /// See the latter for more details. /// private readonly Dictionary RelationshipProxies = new Dictionary(); - public TraversalHelper( IResourceGraph graph, - IJsonApiContext context) + IRequestManager requestManager) { - _context = context; + _requestManager = requestManager; _graph = graph; } @@ -67,9 +69,9 @@ public RootNode CreateRootNode(IEnumerable rootEntiti /// /// The next layer. /// Root node. - public EntityChildLayer CreateNextLayer(IEntityNode rootNode) + public NodeLayer CreateNextLayer(INode rootNode) { - return CreateNextLayer(new IEntityNode[] { rootNode }); + return CreateNextLayer(new INode[] { rootNode }); } /// @@ -77,7 +79,7 @@ public EntityChildLayer CreateNextLayer(IEntityNode rootNode) /// /// The next layer. /// Nodes. - public EntityChildLayer CreateNextLayer(IEnumerable nodes) + public NodeLayer CreateNextLayer(IEnumerable nodes) { /// first extract entities by parsing populated relationships in the entities /// of previous layer @@ -106,7 +108,7 @@ public EntityChildLayer CreateNextLayer(IEnumerable nodes) }).ToList(); /// wrap the child nodes in a EntityChildLayer - return new EntityChildLayer(nextNodes); + return new NodeLayer(nextNodes); } /// @@ -122,7 +124,7 @@ Dictionary - (Dictionary>, Dictionary>) ExtractEntities(IEnumerable principalNodes) + (Dictionary>, Dictionary>) ExtractEntities(IEnumerable principalNodes) { var principalsEntitiesGrouped = new Dictionary>(); // RelationshipAttr_prevlayer->currentlayer => prevLayerEntities var dependentsEntitiesGrouped = new Dictionary>(); // RelationshipAttr_prevlayer->currentlayer => currentLayerEntities @@ -209,6 +211,7 @@ void RegisterRelationshipProxies(DependentType type) DependentType dependentType = GetDependentTypeFromRelationship(attr); bool isContextRelation = false; if (_context.RelationshipsToUpdate != null) isContextRelation = _context.RelationshipsToUpdate.ContainsKey(attr); + if (_context.RelationshipsToUpdate != null) isContextRelation = _context.RelationshipsToUpdate.ContainsKey(attr); var proxy = new RelationshipProxy(attr, dependentType, isContextRelation); RelationshipProxies[attr] = proxy; } @@ -285,10 +288,10 @@ void AddToRelationshipGroup(Dictionary> t /// /// Reflective helper method to create an instance of ; /// - IEntityNode CreateNodeInstance(DependentType nodeType, RelationshipProxy[] relationshipsToNext, IEnumerable relationshipsFromPrev) + INode CreateNodeInstance(DependentType nodeType, RelationshipProxy[] relationshipsToNext, IEnumerable relationshipsFromPrev) { IRelationshipsFromPreviousLayer prev = CreateRelationshipsFromInstance(nodeType, relationshipsFromPrev); - return (IEntityNode)TypeHelper.CreateInstanceOfOpenType(typeof(ChildNode<>), nodeType, new object[] { relationshipsToNext, prev }); + return (INode)TypeHelper.CreateInstanceOfOpenType(typeof(ChildNode<>), nodeType, new object[] { relationshipsToNext, prev }); } /// @@ -316,21 +319,21 @@ IRelationshipGroup CreateRelationshipGroupInstance(Type thisLayerType, Relations /// A helper class that represents all entities in the current layer that /// are being traversed for which hooks will be executed (see IResourceHookExecutor) /// - internal class EntityChildLayer : IEnumerable + internal class NodeLayer : IEnumerable { - readonly List _collection; + readonly List _collection; public bool AnyEntities() { return _collection.Any(n => n.UniqueEntities.Cast().Any()); } - public EntityChildLayer(List nodes) + public NodeLayer(List nodes) { _collection = nodes; } - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { return _collection.GetEnumerator(); } diff --git a/src/JsonApiDotNetCore/Internal/Contracts/IResourceGraph.cs b/src/JsonApiDotNetCore/Internal/Contracts/IResourceGraph.cs new file mode 100644 index 0000000000..71819d2666 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Contracts/IResourceGraph.cs @@ -0,0 +1,93 @@ +using JsonApiDotNetCore.Models; +using System; +using System.Collections.Generic; +using System.Text; + +namespace JsonApiDotNetCore.Internal.Contracts +{ + /// + /// A cache for the models in entity core + /// + public interface IResourceGraph + { + + + RelationshipAttribute GetInverseRelationship(RelationshipAttribute relationship); + /// + /// Gets the value of the navigation property, defined by the relationshipName, + /// on the provided instance. + /// + /// The resource instance + /// The navigation property name. + /// + /// + /// _graph.GetRelationship(todoItem, nameof(TodoItem.Owner)); + /// + /// + /// + /// In the case of a `HasManyThrough` relationship, it will not traverse the relationship + /// and will instead return the value of the shadow property (e.g. Articles.Tags). + /// If you want to traverse the relationship, you should use . + /// + object GetRelationship(TParent resource, string propertyName); + + /// + /// Get the entity type based on a string + /// + /// + /// The context entity from the resource graph + ContextEntity GetEntityType(string entityName); + + /// + /// Gets the value of the navigation property (defined by the ) + /// on the provided instance. + /// In the case of `HasManyThrough` relationships, it will traverse the through entity and return the + /// value of the relationship on the other side of a join entity (e.g. Articles.ArticleTags.Tag). + /// + /// The resource instance + /// The attribute used to define the relationship. + /// + /// + /// _graph.GetRelationshipValue(todoItem, nameof(TodoItem.Owner)); + /// + /// + object GetRelationshipValue(TParent resource, RelationshipAttribute relationship) where TParent : IIdentifiable; + + /// + /// Get the internal navigation property name for the specified public + /// relationship name. + /// + /// The public relationship name specified by a or + /// + /// + /// _graph.GetRelationshipName<TodoItem>("achieved-date"); + /// // returns "AchievedDate" + /// + /// + string GetRelationshipName(string relationshipName); + + /// + /// Get the resource metadata by the DbSet property name + /// + ContextEntity GetContextEntity(string dbSetName); + + /// + /// Get the resource metadata by the resource type + /// + ContextEntity GetContextEntity(Type entityType); + + /// + /// Get the public attribute name for a type based on the internal attribute name. + /// + /// The internal attribute name for a . + string GetPublicAttributeName(string internalAttributeName); + + /// + /// Was built against an EntityFrameworkCore DbContext ? + /// + bool UsesDbContext { get; } + List IncludedRelationships { get; set; } + + ContextEntity GetEntityBasedOnPath(string pathParsed); + } +} diff --git a/src/JsonApiDotNetCore/Internal/InverseRelationships.cs b/src/JsonApiDotNetCore/Internal/InverseRelationships.cs index bec80243d9..fbfd4b9da7 100644 --- a/src/JsonApiDotNetCore/Internal/InverseRelationships.cs +++ b/src/JsonApiDotNetCore/Internal/InverseRelationships.cs @@ -1,5 +1,6 @@ using System; using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; diff --git a/src/JsonApiDotNetCore/Internal/PageManager.cs b/src/JsonApiDotNetCore/Internal/PageManager.cs index d27fc158fd..6e9d3e6a49 100644 --- a/src/JsonApiDotNetCore/Internal/PageManager.cs +++ b/src/JsonApiDotNetCore/Internal/PageManager.cs @@ -1,11 +1,31 @@ using System; using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Internal { - public class PageManager + public class PageManager : IPageManager { + private ILinkBuilder _linkBuilder; + private IJsonApiOptions _options; + + public PageManager(ILinkBuilder linkBuilder, IJsonApiOptions options, IRequestManager requestManager) + { + _linkBuilder = linkBuilder; + _options = options; + if (requestManager.QuerySet != null) + { + PageSize = requestManager.QuerySet?.PageQuery.PageSize != null ? requestManager.QuerySet.PageQuery.PageSize : _options.DefaultPageSize; + } + else + { + PageSize = _options.DefaultPageSize; + } + + DefaultPageSize = _options.DefaultPageSize; + } public int? TotalRecords { get; set; } public int PageSize { get; set; } public int DefaultPageSize { get; set; } @@ -13,7 +33,7 @@ public class PageManager public bool IsPaginated => PageSize > 0; public int TotalPages => (TotalRecords == null) ? -1 : (int)Math.Ceiling(decimal.Divide(TotalRecords.Value, PageSize)); - public RootLinks GetPageLinks(LinkBuilder linkBuilder) + public RootLinks GetPageLinks() { if (ShouldIncludeLinksObject()) return null; @@ -21,16 +41,16 @@ public RootLinks GetPageLinks(LinkBuilder linkBuilder) var rootLinks = new RootLinks(); if (CurrentPage > 1) - rootLinks.First = linkBuilder.GetPageLink(1, PageSize); + rootLinks.First = _linkBuilder.GetPageLink(1, PageSize); if (CurrentPage > 1) - rootLinks.Prev = linkBuilder.GetPageLink(CurrentPage - 1, PageSize); + rootLinks.Prev = _linkBuilder.GetPageLink(CurrentPage - 1, PageSize); if (CurrentPage < TotalPages) - rootLinks.Next = linkBuilder.GetPageLink(CurrentPage + 1, PageSize); + rootLinks.Next = _linkBuilder.GetPageLink(CurrentPage + 1, PageSize); if (TotalPages > 0) - rootLinks.Last = linkBuilder.GetPageLink(TotalPages, PageSize); + rootLinks.Last = _linkBuilder.GetPageLink(TotalPages, PageSize); return rootLinks; } diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs index 3205e1e01a..348469343a 100644 --- a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs @@ -1,4 +1,6 @@ using System; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; @@ -7,9 +9,10 @@ namespace JsonApiDotNetCore.Internal.Query public class AttrFilterQuery : BaseFilterQuery { public AttrFilterQuery( - IJsonApiContext jsonApiContext, + IRequestManager requestManager, + IResourceGraph resourceGraph, FilterQuery filterQuery) - : base(jsonApiContext, filterQuery) + : base(requestManager, resourceGraph, filterQuery) { if (Attribute == null) throw new JsonApiException(400, $"'{filterQuery.Attribute}' is not a valid attribute."); diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs index 341b7e15c0..9f78bd2d5d 100644 --- a/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs @@ -4,10 +4,8 @@ namespace JsonApiDotNetCore.Internal.Query { public class AttrSortQuery : BaseAttrQuery { - public AttrSortQuery( - IJsonApiContext jsonApiContext, - SortQuery sortQuery) - :base(jsonApiContext, sortQuery) + public AttrSortQuery(IJsonApiContext jsonApiContext,SortQuery sortQuery) + :base(jsonApiContext.RequestManager,jsonApiContext.ResourceGraph, sortQuery) { if (Attribute == null) throw new JsonApiException(400, $"'{sortQuery.Attribute}' is not a valid attribute."); diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs index e7eeb83e68..44d39ba002 100644 --- a/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs @@ -1,3 +1,5 @@ +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using System; @@ -12,24 +14,19 @@ namespace JsonApiDotNetCore.Internal.Query /// public abstract class BaseAttrQuery { - private readonly IJsonApiContext _jsonApiContext; + private readonly IRequestManager _requestManager; + private readonly IResourceGraph _resourceGraph; - public BaseAttrQuery(IJsonApiContext jsonApiContext, BaseQuery baseQuery) + public BaseAttrQuery(IRequestManager requestManager, IResourceGraph resourceGraph, BaseQuery baseQuery) { - _jsonApiContext = jsonApiContext ?? throw new ArgumentNullException(nameof(jsonApiContext)); - - if(_jsonApiContext.RequestEntity == null) - throw new ArgumentException($"{nameof(IJsonApiContext)}.{nameof(_jsonApiContext.RequestEntity)} cannot be null. " - + "This property contains the ResourceGraph node for the requested entity. " - + "If this is a unit test, you need to mock this member. " - + "See this issue to check the current status of improved test guidelines: " - + "https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/251", nameof(jsonApiContext)); + _requestManager = requestManager ?? throw new ArgumentNullException(nameof(requestManager)); + _resourceGraph = resourceGraph ?? throw new ArgumentNullException(nameof(resourceGraph)); - if(_jsonApiContext.ResourceGraph == null) - throw new ArgumentException($"{nameof(IJsonApiContext)}.{nameof(_jsonApiContext.ResourceGraph)} cannot be null. " + if(_resourceGraph == null) + throw new ArgumentException($"{nameof(IJsonApiContext)}.{nameof(_resourceGraph)} cannot be null. " + "If this is a unit test, you need to construct a graph containing the resources being tested. " + "See this issue to check the current status of improved test guidelines: " - + "https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/251", nameof(jsonApiContext)); + + "https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/251", nameof(requestManager)); if (baseQuery.IsAttributeOfRelationship) { @@ -56,14 +53,18 @@ public string GetPropertyPath() } private AttrAttribute GetAttribute(string attribute) - => _jsonApiContext.RequestEntity.Attributes.FirstOrDefault(attr => attr.Is(attribute)); + { + return _requestManager.GetContextEntity().Attributes.FirstOrDefault(attr => attr.Is(attribute)); + } private RelationshipAttribute GetRelationship(string propertyName) - => _jsonApiContext.RequestEntity.Relationships.FirstOrDefault(r => r.Is(propertyName)); + { + return _requestManager.GetContextEntity().Relationships.FirstOrDefault(r => r.Is(propertyName)); + } private AttrAttribute GetAttribute(RelationshipAttribute relationship, string attribute) { - var relatedContextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.DependentType); + var relatedContextEntity = _resourceGraph.GetContextEntity(relationship.DependentType); return relatedContextEntity.Attributes .FirstOrDefault(a => a.Is(attribute)); } diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs index f7e308369e..1a12d50d67 100644 --- a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs @@ -1,14 +1,20 @@ +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Services; using System; namespace JsonApiDotNetCore.Internal.Query { + /// + /// Is the base for all filter queries + /// public class BaseFilterQuery : BaseAttrQuery { public BaseFilterQuery( - IJsonApiContext jsonApiContext, + IRequestManager requestManager, + IResourceGraph resourceGraph, FilterQuery filterQuery) - : base(jsonApiContext, filterQuery) + : base(requestManager, resourceGraph, filterQuery) { PropertyValue = filterQuery.Value; FilterOperation = GetFilterOperation(filterQuery.Operation); diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs index 8fef8de693..a6e02b13b9 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs @@ -1,4 +1,6 @@ using System; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; @@ -7,19 +9,22 @@ namespace JsonApiDotNetCore.Internal.Query public class RelatedAttrFilterQuery : BaseFilterQuery { public RelatedAttrFilterQuery( - IJsonApiContext jsonApiContext, + IRequestManager requestManager, + IResourceGraph resourceGraph, FilterQuery filterQuery) - :base(jsonApiContext, filterQuery) + : base(requestManager: requestManager, + resourceGraph: resourceGraph, + filterQuery: filterQuery) { if (Relationship == null) - throw new JsonApiException(400, $"{filterQuery.Relationship} is not a valid relationship on {jsonApiContext.RequestEntity.EntityName}."); + throw new JsonApiException(400, $"{filterQuery.Relationship} is not a valid relationship on {requestManager.GetContextEntity().EntityName}."); if (Attribute == null) throw new JsonApiException(400, $"'{filterQuery.Attribute}' is not a valid attribute."); if (Attribute.IsFilterable == false) throw new JsonApiException(400, $"Filter is not allowed for attribute '{Attribute.PublicAttributeName}'."); - + } } } diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs index 4215382c80..8c54581693 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs @@ -7,7 +7,7 @@ public class RelatedAttrSortQuery : BaseAttrQuery public RelatedAttrSortQuery( IJsonApiContext jsonApiContext, SortQuery sortQuery) - :base(jsonApiContext, sortQuery) + :base(jsonApiContext.RequestManager, jsonApiContext.ResourceGraph, sortQuery) { if (Relationship == null) throw new JsonApiException(400, $"{sortQuery.Relationship} is not a valid relationship on {jsonApiContext.RequestEntity.EntityName}."); diff --git a/src/JsonApiDotNetCore/Internal/ResourceGraph.cs b/src/JsonApiDotNetCore/Internal/ResourceGraph.cs index efec077536..582dab12ad 100644 --- a/src/JsonApiDotNetCore/Internal/ResourceGraph.cs +++ b/src/JsonApiDotNetCore/Internal/ResourceGraph.cs @@ -2,93 +2,34 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Internal { - public interface IResourceGraph + public class ControllerModelMap { - /// - /// Gets the value of the navigation property, defined by the relationshipName, - /// on the provided instance. - /// - /// The resource instance - /// The navigation property name. - /// - /// - /// _graph.GetRelationship(todoItem, nameof(TodoItem.Owner)); - /// - /// - /// - /// In the case of a `HasManyThrough` relationship, it will not traverse the relationship - /// and will instead return the value of the shadow property (e.g. Articles.Tags). - /// If you want to traverse the relationship, you should use . - /// - object GetRelationship(TParent resource, string propertyName); - - /// - /// Gets the value of the navigation property (defined by the ) - /// on the provided instance. - /// In the case of `HasManyThrough` relationships, it will traverse the through entity and return the - /// value of the relationship on the other side of a join entity (e.g. Articles.ArticleTags.Tag). - /// - /// The resource instance - /// The attribute used to define the relationship. - /// - /// - /// _graph.GetRelationshipValue(todoItem, nameof(TodoItem.Owner)); - /// - /// - object GetRelationshipValue(TParent resource, RelationshipAttribute relationship) where TParent : IIdentifiable; - - /// - /// Get the internal navigation property name for the specified public - /// relationship name. - /// - /// The public relationship name specified by a or - /// - /// - /// _graph.GetRelationshipName<TodoItem>("achieved-date"); - /// // returns "AchievedDate" - /// - /// - string GetRelationshipName(string relationshipName); - - /// - /// Get the resource metadata by the DbSet property name - /// - ContextEntity GetContextEntity(string dbSetName); - - /// - /// Get the resource metadata by the resource type - /// - ContextEntity GetContextEntity(Type entityType); - - /// - /// Get the public attribute name for a type based on the internal attribute name. - /// - /// The internal attribute name for a . - string GetPublicAttributeName(string internalAttributeName); - - /// - /// Helper method to get the inverse relationship attribute corresponding - /// to a relationship. - /// - RelationshipAttribute GetInverseRelationship(RelationshipAttribute relationship); - - /// - /// Was built against an EntityFrameworkCore DbContext ? - /// - bool UsesDbContext { get; } + public Type Controller { get; set; } + public Type Model { get; set; } + public string Path { get; set; } + } + /// + /// keeps track of all the models/resources defined in JADNC + /// public class ResourceGraph : IResourceGraph { internal List Entities { get; } internal List ValidationResults { get; } + + public List ControllerModelMap { get; internal set; } + + [Obsolete("please instantiate properly, dont use the static constructor")] internal static IResourceGraph Instance { get; set; } public ResourceGraph() { } + [Obsolete("Use new one")] public ResourceGraph(List entities, bool usesDbContext) { Entities = entities; @@ -97,12 +38,18 @@ public ResourceGraph(List entities, bool usesDbContext) Instance = this; } + public ContextEntity GetEntityType(string entityName) + { + return Entities.Where(e => e.EntityName == entityName).FirstOrDefault(); + } + // eventually, this is the planned public constructor // to avoid breaking changes, we will be leaving the original constructor in place // until the context graph validation process is completed // you can track progress on this issue here: https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/170 - internal ResourceGraph(List entities, bool usesDbContext, List validationResults) + internal ResourceGraph(List entities, bool usesDbContext, List validationResults, List controllerContexts) { + ControllerModelMap = controllerContexts; Entities = entities; UsesDbContext = usesDbContext; ValidationResults = validationResults; @@ -111,6 +58,7 @@ internal ResourceGraph(List entities, bool usesDbContext, List public bool UsesDbContext { get; } + public List IncludedRelationships { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } /// public ContextEntity GetContextEntity(string entityName) @@ -137,7 +85,7 @@ public object GetRelationship(TParent entity, string relationshipName) public object GetRelationshipValue(TParent resource, RelationshipAttribute relationship) where TParent : IIdentifiable { - if(relationship is HasManyThroughAttribute hasManyThroughRelationship) + if (relationship is HasManyThroughAttribute hasManyThroughRelationship) { return GetHasManyThrough(resource, hasManyThroughRelationship); } @@ -189,5 +137,14 @@ public RelationshipAttribute GetInverseRelationship(RelationshipAttribute relati if (relationship.InverseNavigation == null) return null; return GetContextEntity(relationship.DependentType).Relationships.SingleOrDefault(r => r.InternalRelationshipName == relationship.InverseNavigation); } + + public ContextEntity GetEntityBasedOnPath(string pathParsed) + { + var controllerMatches = ControllerModelMap.Where(cm => cm.Controller.Name.ToLower().Contains(pathParsed.ToLower())); + + var model = controllerMatches.First().Model; + + return Entities.Where(e => e.EntityType == model).First(); + } } } diff --git a/src/JsonApiDotNetCore/Managers/Contracts/IPageManager.cs b/src/JsonApiDotNetCore/Managers/Contracts/IPageManager.cs new file mode 100644 index 0000000000..814cdc35a5 --- /dev/null +++ b/src/JsonApiDotNetCore/Managers/Contracts/IPageManager.cs @@ -0,0 +1,35 @@ +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; +using System; +using System.Collections.Generic; +using System.Text; + +namespace JsonApiDotNetCore.Managers.Contracts +{ + public interface IPageManager + { + /// + /// What the total records are for this output + /// + int? TotalRecords { get; set; } + /// + /// How many records per page should be shown + /// + int PageSize { get; set; } + /// + /// What is the default page size + /// + int DefaultPageSize { get; set; } + /// + /// What page are we currently on + /// + int CurrentPage { get; set; } + /// + /// Are we even paginating + /// + bool IsPaginated { get; } + + RootLinks GetPageLinks(); + } +} diff --git a/src/JsonApiDotNetCore/Managers/Contracts/IRequestManager.cs b/src/JsonApiDotNetCore/Managers/Contracts/IRequestManager.cs new file mode 100644 index 0000000000..6a24b652a6 --- /dev/null +++ b/src/JsonApiDotNetCore/Managers/Contracts/IRequestManager.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Text; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Http; + +namespace JsonApiDotNetCore.Managers.Contracts +{ + public interface IRequestManager : IQueryRequest + { + Dictionary GetUpdatedAttributes(); + Dictionary GetUpdatedRelationships(); + /// + /// The request namespace. This may be an absolute or relative path + /// depending upon the configuration. + /// + /// + /// Absolute: https://example.com/api/v1 + /// + /// Relative: /api/v1 + /// + string BasePath { get; set; } + QuerySet QuerySet { get; set; } + IQueryCollection FullQuerySet { get; set; } + + /// + /// If the request is on the `{id}/relationships/{relationshipName}` route + /// + bool IsRelationshipPath { get; set; } + /// + /// Gets the relationships as set in the query parameters + /// + /// + List GetRelationships(); + /// + /// Gets the sparse fields + /// + /// + List GetFields(); + /// + /// Sets the current context entity for this entire request + /// + /// + void SetContextEntity(ContextEntity contextEntityCurrent); + + ContextEntity GetContextEntity(); + /// + /// Which query params are filtered + /// + QueryParams DisabledQueryParams { get; set; } + + } +} diff --git a/src/JsonApiDotNetCore/Managers/Contracts/IResourceGraphManager.cs b/src/JsonApiDotNetCore/Managers/Contracts/IResourceGraphManager.cs new file mode 100644 index 0000000000..3a58f3e2b2 --- /dev/null +++ b/src/JsonApiDotNetCore/Managers/Contracts/IResourceGraphManager.cs @@ -0,0 +1,11 @@ +using JsonApiDotNetCore.Internal; +using System; +using System.Collections.Generic; +using System.Text; + +namespace JsonApiDotNetCore.Managers.Contracts +{ + public interface IResourceGraphManager + { + } +} diff --git a/src/JsonApiDotNetCore/Managers/RequestManager.cs b/src/JsonApiDotNetCore/Managers/RequestManager.cs new file mode 100644 index 0000000000..592a55e724 --- /dev/null +++ b/src/JsonApiDotNetCore/Managers/RequestManager.cs @@ -0,0 +1,77 @@ +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Text; + +namespace JsonApiDotNetCore.Managers +{ + public class UpdatesContainer + { + /// + /// The attributes that were included in a PATCH request. + /// Only the attributes in this dictionary should be updated. + /// + public Dictionary Attributes { get; set; } + + /// + /// Any relationships that were included in a PATCH request. + /// Only the relationships in this dictionary should be updated. + /// + public Dictionary Relationships { get; } + + } + + class RequestManager : IRequestManager + { + private ContextEntity _contextEntity; + private IQueryParser _queryParser; + + public string BasePath { get; set; } + public List IncludedRelationships { get; set; } + public QuerySet QuerySet { get; set; } + public PageManager PageManager { get; set; } + public IQueryCollection FullQuerySet { get; set; } + public QueryParams DisabledQueryParams { get; set; } + public bool IsRelationshipPath { get; set; } + public Dictionary AttributesToUpdate { get; set; } + /// + /// Contains all the information you want about any update occuring + /// + private UpdatesContainer _updatesContainer { get; set; } + public Dictionary RelationshipsToUpdate { get; set; } + + + public Dictionary GetUpdatedAttributes() + { + return _updatesContainer.Attributes; + } + public Dictionary GetUpdatedRelationships() + { + return _updatesContainer.Relationships; + } + public List GetFields() + { + return QuerySet?.Fields; + } + + public List GetRelationships() + { + return QuerySet?.IncludedRelationships; + } + public ContextEntity GetContextEntity() + { + return _contextEntity; + } + + public void SetContextEntity(ContextEntity contextEntityCurrent) + { + _contextEntity = contextEntityCurrent; + } + } +} diff --git a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs index ab472a8dba..9d42f7da0b 100644 --- a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs @@ -1,12 +1,22 @@ using System; +using System.Linq; using System.Threading.Tasks; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; namespace JsonApiDotNetCore.Middleware { + /// + /// Can be overwritten to help you out during testing + /// + /// This sets all necessary paaramters relating to the HttpContext for JADNC + /// public class RequestMiddleware { private readonly RequestDelegate _next; @@ -16,7 +26,12 @@ public RequestMiddleware(RequestDelegate next) _next = next; } - public async Task Invoke(HttpContext context, IJsonApiContext jsonApiContext) + public async Task Invoke(HttpContext context, + IJsonApiContext jsonApiContext, + IResourceGraph resourceGraph, + IRequestManager requestManager, + IQueryParser queryParser, + IJsonApiOptions options) { if (IsValid(context)) { @@ -25,9 +40,122 @@ public async Task Invoke(HttpContext context, IJsonApiContext jsonApiContext) // since the JsonApiContext is using field initializers // Need to work on finding a better solution. jsonApiContext.BeginOperation(); + ContextEntity contextEntityCurrent = GetCurrentEntity(context.Request.Path, resourceGraph, options); + requestManager.SetContextEntity(contextEntityCurrent); + requestManager.BasePath = GetBasePath(context, options, contextEntityCurrent?.EntityName); + //Handle all querySet + HandleUriParameters(context, queryParser, requestManager); + requestManager.IsRelationshipPath = PathIsRelationship(context.Request.Path.Value); + // BACKWARD COMPATIBILITY for v4 will be removed in v5 + jsonApiContext.RequestManager = requestManager; + jsonApiContext.PageManager = new PageManager(new LinkBuilder(options, requestManager), options, requestManager); await _next(context); } } + /// + /// Parses the uri, and helps you out + /// + /// + /// + protected void HandleUriParameters(HttpContext context, IQueryParser queryParser, IRequestManager requestManager) + { + if (context.Request.Query.Count > 0) + { + //requestManager.FullQuerySet = context.Request.Query; + requestManager.QuerySet = queryParser.Parse(context.Request.Query); + requestManager.IncludedRelationships = requestManager.QuerySet.IncludedRelationships; + } + } + internal static bool PathIsRelationship(string requestPath) + { + // while(!Debugger.IsAttached) { Thread.Sleep(1000); } + const string relationships = "relationships"; + const char pathSegmentDelimiter = '/'; + + var span = requestPath.AsSpan(); + + // we need to iterate over the string, from the end, + // checking whether or not the 2nd to last path segment + // is "relationships" + // -2 is chosen in case the path ends with '/' + for (var i = requestPath.Length - 2; i >= 0; i--) + { + // if there are not enough characters left in the path to + // contain "relationships" + if (i < relationships.Length) + return false; + + // we have found the first instance of '/' + if (span[i] == pathSegmentDelimiter) + { + // in the case of a "relationships" route, the next + // path segment will be "relationships" + return ( + span.Slice(i - relationships.Length, relationships.Length) + .SequenceEqual(relationships.AsSpan()) + ); + } + } + + return false; + } + private string GetBasePath(HttpContext context, IJsonApiOptions options, string entityName) + { + var r = context.Request; + if (options.RelativeLinks) + { + return GetNamespaceFromPath(r.Path, entityName); + } + else + { + return $"{r.Scheme}://{r.Host}{GetNamespaceFromPath(r.Path, entityName)}"; + } + } + internal static string GetNamespaceFromPath(string path, string entityName) + { + var entityNameSpan = entityName.AsSpan(); + var pathSpan = path.AsSpan(); + const char delimiter = '/'; + for (var i = 0; i < pathSpan.Length; i++) + { + if (pathSpan[i].Equals(delimiter)) + { + var nextPosition = i + 1; + if (pathSpan.Length > i + entityNameSpan.Length) + { + var possiblePathSegment = pathSpan.Slice(nextPosition, entityNameSpan.Length); + if (entityNameSpan.SequenceEqual(possiblePathSegment)) + { + // check to see if it's the last position in the string + // or if the next character is a / + var lastCharacterPosition = nextPosition + entityNameSpan.Length; + + if (lastCharacterPosition == pathSpan.Length || pathSpan.Length >= lastCharacterPosition + 2 && pathSpan[lastCharacterPosition].Equals(delimiter)) + { + return pathSpan.Slice(0, i).ToString(); + } + } + } + } + } + + return string.Empty; + } + /// + /// Gets the current entity that we need for serialization and deserialization. + /// + /// + /// + /// + private ContextEntity GetCurrentEntity(PathString path, IResourceGraph resourceGraph, IJsonApiOptions options) + { + var pathParsed = path.ToString().Replace($"{options.Namespace}/", ""); + if(pathParsed[0] == '/') + { + pathParsed = pathParsed.Substring(1); + } + return resourceGraph.GetEntityBasedOnPath(pathParsed); + } private static bool IsValid(HttpContext context) { diff --git a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs index 0a6b0ac091..2a5a37ca8b 100644 --- a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs @@ -1,4 +1,5 @@ using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Hooks; using System; diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index c11c011852..f53a9b752e 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -184,9 +184,15 @@ private object SetRelationships( foreach (var attr in contextEntity.Relationships) { - entity = attr.IsHasOne - ? SetHasOneRelationship(entity, entityProperties, (HasOneAttribute)attr, contextEntity, relationships, included) - : SetHasManyRelationship(entity, entityProperties, (HasManyAttribute)attr, contextEntity, relationships, included); + if (attr.IsHasOne) + { + SetHasOneRelationship(entity, entityProperties, (HasOneAttribute)attr, contextEntity, relationships, included); + } + else + { + SetHasManyRelationship(entity, entityProperties, (HasManyAttribute)attr, contextEntity, relationships, included); + } + } return entity; diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs index a784554f58..2729e1ec94 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.Extensions.Logging; @@ -12,6 +13,7 @@ public class JsonApiSerializer : IJsonApiSerializer { private readonly IDocumentBuilder _documentBuilder; private readonly ILogger _logger; + private readonly IRequestManager _requestManager; private readonly IJsonApiContext _jsonApiContext; public JsonApiSerializer( @@ -24,9 +26,11 @@ public JsonApiSerializer( public JsonApiSerializer( IJsonApiContext jsonApiContext, + IRequestManager requestManager, IDocumentBuilder documentBuilder, ILoggerFactory loggerFactory) { + _requestManager = requestManager; _jsonApiContext = jsonApiContext; _documentBuilder = documentBuilder; _logger = loggerFactory?.CreateLogger(); @@ -37,7 +41,7 @@ public string Serialize(object entity) if (entity == null) return GetNullDataResponse(); - if (entity.GetType() == typeof(ErrorCollection) || (_jsonApiContext.RequestEntity == null && _jsonApiContext.IsBulkOperationRequest == false)) + if (entity.GetType() == typeof(ErrorCollection) || (_requestManager.GetContextEntity()== null && _jsonApiContext.IsBulkOperationRequest == false)) return GetErrorJson(entity, _logger); if (_jsonApiContext.IsBulkOperationRequest) diff --git a/src/JsonApiDotNetCore/Services/EntityResourceService.cs b/src/JsonApiDotNetCore/Services/EntityResourceService.cs index c889af68b8..3a80990451 100644 --- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs +++ b/src/JsonApiDotNetCore/Services/EntityResourceService.cs @@ -1,5 +1,7 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Data; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Hooks; using Microsoft.Extensions.Logging; @@ -7,57 +9,55 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using JsonApiDotNetCore.Internal.Contracts; namespace JsonApiDotNetCore.Services { - public class EntityResourceService : EntityResourceService, - IResourceService - where TResource : class, IIdentifiable - { - public EntityResourceService( - IJsonApiContext jsonApiContext, - IEntityRepository entityRepository, - ILoggerFactory loggerFactory = null, - IResourceHookExecutor hookExecutor = null) : - base(jsonApiContext, entityRepository, loggerFactory, hookExecutor) - { } - } - - public class EntityResourceService : EntityResourceService, - IResourceService - where TResource : class, IIdentifiable - { - public EntityResourceService( - IJsonApiContext jsonApiContext, - IEntityRepository entityRepository, - ILoggerFactory loggerFactory = null, - IResourceHookExecutor hookExecutor = null) : - base(jsonApiContext, entityRepository, hookExecutor, loggerFactory) - { } - } - + /// + /// Entity mapping class + /// + /// + /// + /// public class EntityResourceService : IResourceService where TResource : class, IIdentifiable where TEntity : class, IIdentifiable { - private readonly IJsonApiContext _jsonApiContext; - private readonly IEntityRepository _entities; + private readonly IPageManager _pageManager; + private readonly IRequestManager _requestManager; + private readonly IJsonApiOptions _options; + private readonly IResourceGraph _resourceGraph; + private readonly IEntityRepository _repository; private readonly ILogger _logger; private readonly IResourceMapper _mapper; private readonly IResourceHookExecutor _hookExecutor; - + /// + /// Base constructor where we assign the variables + /// + /// + /// + /// + /// + /// + /// + /// public EntityResourceService( - IJsonApiContext jsonApiContext, - IEntityRepository entityRepository, - IResourceHookExecutor hookExecutor = null, - IResourceMapper mapper = null, - ILoggerFactory loggerFactory = null) + IEntityRepository repository, + IJsonApiOptions options, + IRequestManager requestManager, + IPageManager pageManager, + IResourceGraph resourceGraph, + IResourceHookExecutor hookExecutor = null, + IResourceMapper mapper = null, + ILoggerFactory loggerFactory = null) { - _jsonApiContext = jsonApiContext; - _entities = entityRepository; - + _requestManager = requestManager; + _pageManager = pageManager; + _options = options; + _resourceGraph = resourceGraph; + _repository = repository; if (mapper == null && typeof(TResource) != typeof(TEntity)) { throw new InvalidOperationException("Resource and Entity types are NOT the same. Please provide a mapper."); @@ -67,41 +67,18 @@ public EntityResourceService( _logger = loggerFactory?.CreateLogger>(); } - - public EntityResourceService( - IJsonApiContext jsonApiContext, - IEntityRepository entityRepository, - IResourceHookExecutor hookExecutor, - ILoggerFactory loggerFactory = null) : this(jsonApiContext, entityRepository, hookExecutor: hookExecutor, mapper: null, loggerFactory: null) - { } - - public EntityResourceService( - IJsonApiContext jsonApiContext, - IEntityRepository entityRepository, - ILoggerFactory loggerFactory = null) : this(jsonApiContext, entityRepository, hookExecutor: null, mapper: null, loggerFactory: loggerFactory) - { } - - [Obsolete("Use ctor with signature (jsonApiContext, entityRepository, hookExecutor = null, mapper = null, loggerFactory = null")] - public EntityResourceService( - IJsonApiContext jsonApiContext, - IEntityRepository entityRepository, - ILoggerFactory loggerFactory, - IResourceMapper mapper ) : this(jsonApiContext, entityRepository, hookExecutor: null, mapper: mapper, loggerFactory: loggerFactory) - { } - public virtual async Task CreateAsync(TResource resource) { var entity = MapIn(resource); - entity = IsNull(_hookExecutor) ? entity : _hookExecutor.BeforeCreate(AsList(entity), ResourcePipeline.Post).SingleOrDefault(); - entity = await _entities.CreateAsync(entity); + entity = await _repository.CreateAsync(entity); // this ensures relationships get reloaded from the database if they have // been requested // https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/343 - if (ShouldIncludeRelationships()) + if (ShouldRelationshipsBeIncluded()) { - if (_entities is IEntityFrameworkRepository efRepository) + if (_repository is IEntityFrameworkRepository efRepository) efRepository.DetachRelationshipPointers(entity); entity = await GetWithRelationshipsAsync(entity.Id); @@ -119,25 +96,24 @@ public virtual async Task DeleteAsync(TId id) var entity = (TEntity)Activator.CreateInstance(typeof(TEntity)); entity.Id = id; if (!IsNull(_hookExecutor, entity)) _hookExecutor.BeforeDelete(AsList(entity), ResourcePipeline.Delete); - var succeeded = await _entities.DeleteAsync(entity.Id); + var succeeded = await _repository.DeleteAsync(entity.Id); if (!IsNull(_hookExecutor, entity)) _hookExecutor.AfterDelete(AsList(entity), ResourcePipeline.Delete, succeeded); return succeeded; } - public virtual async Task> GetAsync() { _hookExecutor?.BeforeRead(ResourcePipeline.Get); - var entities = _entities.Get(); + var entities = _repository.Get(); entities = ApplySortAndFilterQuery(entities); if (ShouldIncludeRelationships()) - entities = IncludeRelationships(entities, _jsonApiContext.QuerySet.IncludedRelationships); - - if (_jsonApiContext.Options.IncludeTotalRecordCount) - _jsonApiContext.PageManager.TotalRecords = await _entities.CountAsync(entities); + entities = IncludeRelationships(entities, _requestManager.QuerySet.IncludedRelationships); + + if (_options.IncludeTotalRecordCount) + _pageManager.TotalRecords = await _repository.CountAsync(entities); - entities = _entities.Select(entities, _jsonApiContext.QuerySet?.Fields); + entities = _repository.Select(entities, _requestManager.QuerySet?.Fields); if (!IsNull(_hookExecutor, entities)) { @@ -146,8 +122,8 @@ public virtual async Task> GetAsync() entities = _hookExecutor.OnReturn(result, ResourcePipeline.Get).AsQueryable(); } - if (_jsonApiContext.Options.IncludeTotalRecordCount) - _jsonApiContext.PageManager.TotalRecords = await _entities.CountAsync(entities); + if (_options.IncludeTotalRecordCount) + _pageManager.TotalRecords = await _repository.CountAsync(entities); // pagination should be done last since it will execute the query var pagedEntities = await ApplyPageQueryAsync(entities); @@ -165,13 +141,14 @@ public virtual async Task GetAsync(TId id) } else { - entity = await _entities.GetAsync(id); + entity = await _repository.GetAsync(id); } - if(!IsNull(_hookExecutor, entity)) + if (!IsNull(_hookExecutor, entity)) { _hookExecutor.AfterRead(AsList(entity), pipeline); entity = _hookExecutor.OnReturn(AsList(entity), pipeline).SingleOrDefault(); } + return MapOut(entity); } @@ -182,9 +159,8 @@ public virtual async Task GetAsync(TId id) // triggered by GET /articles/1/{relationshipName} public virtual async Task GetRelationshipAsync(TId id, string relationshipName) { - _hookExecutor?.BeforeRead(ResourcePipeline.GetRelationship, id.ToString()); - var entity = await _entities.GetAndIncludeAsync(id, relationshipName); + var entity = await _repository.GetAndIncludeAsync(id, relationshipName); if (!IsNull(_hookExecutor, entity)) { _hookExecutor.AfterRead(AsList(entity), ResourcePipeline.GetRelationship); @@ -201,11 +177,11 @@ public virtual async Task GetRelationshipAsync(TId id, string relationsh var resource = MapOut(entity); // compound-property -> CompoundProperty - var navigationPropertyName = _jsonApiContext.ResourceGraph.GetRelationshipName(relationshipName); + var navigationPropertyName = _resourceGraph.GetRelationshipName(relationshipName); if (navigationPropertyName == null) throw new JsonApiException(422, $"Relationship '{relationshipName}' does not exist on resource '{typeof(TResource)}'."); - var relationshipValue = _jsonApiContext.ResourceGraph.GetRelationship(resource, navigationPropertyName); + var relationshipValue = _resourceGraph.GetRelationship(resource, navigationPropertyName); return relationshipValue; } @@ -214,7 +190,7 @@ public virtual async Task UpdateAsync(TId id, TResource resource) var entity = MapIn(resource); entity = IsNull(_hookExecutor) ? entity : _hookExecutor.BeforeUpdate(AsList(entity), ResourcePipeline.Patch).SingleOrDefault(); - entity = await _entities.UpdateAsync(entity); + entity = await _repository.UpdateAsync(entity); if (!IsNull(_hookExecutor, entity)) { _hookExecutor.AfterUpdate(AsList(entity), ResourcePipeline.Patch); @@ -226,13 +202,13 @@ public virtual async Task UpdateAsync(TId id, TResource resource) // triggered by PATCH /articles/1/relationships/{relationshipName} public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipName, List relationships) { - var entity = await _entities.GetAndIncludeAsync(id, relationshipName); + var entity = await _repository.GetAndIncludeAsync(id, relationshipName); if (entity == null) { throw new JsonApiException(404, $"Entity with id {id} could not be found."); } - var relationship = _jsonApiContext.ResourceGraph + var relationship = _resourceGraph .GetContextEntity(typeof(TResource)) .Relationships .FirstOrDefault(r => r.Is(relationshipName)); @@ -255,7 +231,7 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa var relationshipIds = relationships.Select(r => r?.Id?.ToString()); entity = IsNull(_hookExecutor) ? entity : _hookExecutor.BeforeUpdate(AsList(entity), ResourcePipeline.PatchRelationship).SingleOrDefault(); - await _entities.UpdateRelationshipsAsync(entity, relationship, relationshipIds); + await _repository.UpdateRelationshipsAsync(entity, relationship, relationshipIds); if (!IsNull(_hookExecutor, entity)) _hookExecutor.AfterUpdate(AsList(entity), ResourcePipeline.PatchRelationship); relationship.Type = relationshipType; @@ -263,70 +239,91 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa protected virtual async Task> ApplyPageQueryAsync(IQueryable entities) { - var pageManager = _jsonApiContext.PageManager; - if (!pageManager.IsPaginated) + if (!_pageManager.IsPaginated) { - var allEntities = await _entities.ToListAsync(entities); + var allEntities = await _repository.ToListAsync(entities); return (typeof(TResource) == typeof(TEntity)) ? allEntities as IEnumerable : _mapper.Map>(allEntities); } if (_logger?.IsEnabled(LogLevel.Information) == true) { - _logger?.LogInformation($"Applying paging query. Fetching page {pageManager.CurrentPage} " + - $"with {pageManager.PageSize} entities"); + _logger?.LogInformation($"Applying paging query. Fetching page {_pageManager.CurrentPage} " + + $"with {_pageManager.PageSize} entities"); } - var pagedEntities = await _entities.PageAsync(entities, pageManager.PageSize, pageManager.CurrentPage); + var pagedEntities = await _repository.PageAsync(entities, _pageManager.PageSize, _pageManager.CurrentPage); return MapOut(pagedEntities); } protected virtual IQueryable ApplySortAndFilterQuery(IQueryable entities) { - var query = _jsonApiContext.QuerySet; + var query = _requestManager.QuerySet; - if (_jsonApiContext.QuerySet == null) + if (_requestManager.QuerySet == null) return entities; if (query.Filters.Count > 0) foreach (var filter in query.Filters) - entities = _entities.Filter(entities, filter); + entities = _repository.Filter(entities, filter); - entities = _entities.Sort(entities, query.SortParameters); + entities = _repository.Sort(entities, query.SortParameters); return entities; } + /// + /// Actually include the relationships + /// + /// + /// + /// protected virtual IQueryable IncludeRelationships(IQueryable entities, List relationships) { - _jsonApiContext.IncludedRelationships = relationships; + _resourceGraph.IncludedRelationships = relationships; foreach (var r in relationships) - entities = _entities.Include(entities, r); + { + entities = _repository.Include(entities, r); + } return entities; } + /// + /// Get the specified id with relationships (async) + /// + /// + /// private async Task GetWithRelationshipsAsync(TId id) { - var query = _entities.Select(_entities.Get(), _jsonApiContext.QuerySet?.Fields).Where(e => e.Id.Equals(id)); + var query = _repository.Select(_repository.Get(), _requestManager.QuerySet?.Fields).Where(e => e.Id.Equals(id)); - _jsonApiContext.QuerySet.IncludedRelationships.ForEach(r => + _requestManager.GetRelationships().ForEach((Action)(r => { - query = _entities.Include(query, r); - }); + query = this._repository.Include((IQueryable)query, r); + })); TEntity value; // https://github.com/aspnet/EntityFrameworkCore/issues/6573 - if (_jsonApiContext.QuerySet?.Fields?.Count > 0) + if (_requestManager.GetFields()?.Count() > 0) + { value = query.FirstOrDefault(); + } else - value = await _entities.FirstOrDefaultAsync(query); + { + value = await _repository.FirstOrDefaultAsync(query); + } return value; } + private bool ShouldIncludeRelationships() + { + return _requestManager.GetRelationships()?.Count() > 0; + } + private bool IsNull(params object[] values) { @@ -337,14 +334,24 @@ private bool IsNull(params object[] values) return false; } - private bool ShouldIncludeRelationships() - => (_jsonApiContext.QuerySet?.IncludedRelationships != null && - _jsonApiContext.QuerySet.IncludedRelationships.Count > 0); + /// + /// Should the relationships be included? + /// + /// + private bool ShouldRelationshipsBeIncluded() + { + return _requestManager.GetRelationships()?.Count() > 0; + } + /// + /// Casts the entity given to `TResource` or maps it to its equal + /// + /// + /// private TResource MapOut(TEntity entity) - => (typeof(TResource) == typeof(TEntity)) - ? entity as TResource : - _mapper.Map(entity); + { + return (typeof(TResource) == typeof(TEntity)) ? entity as TResource : _mapper.Map(entity); + } private IEnumerable MapOut(IEnumerable entities) => (typeof(TResource) == typeof(TEntity)) @@ -361,4 +368,63 @@ private List AsList(TEntity entity) return new List { entity }; } } + /// + /// No mapping + /// + /// + /// + public class EntityResourceService : EntityResourceService, + IResourceService + where TResource : class, IIdentifiable + { + public EntityResourceService( + IEntityRepository repository, + IJsonApiOptions apiOptions, + IRequestManager requestManager, + IResourceGraph resourceGraph, + IPageManager pageManager, + ILoggerFactory loggerFactory = null, + IResourceHookExecutor hookExecutor = null) + : base(repository: repository, + options: apiOptions, + requestManager: requestManager, + pageManager: pageManager, + loggerFactory: loggerFactory, + resourceGraph: resourceGraph, + hookExecutor: hookExecutor) + { } + } + + /// + /// No mapping with integer as default + /// + /// + public class EntityResourceService : EntityResourceService, + IResourceService + where TResource : class, IIdentifiable + { + /// + /// Constructor for no mapping with integer as dfeault + /// + /// + /// + /// + /// + /// + /// + public EntityResourceService( + IEntityRepository repository, + IJsonApiOptions options, + IRequestManager requestManager, + IPageManager pageManager, + IResourceGraph resourceGraph, + ILoggerFactory loggerFactory = null, + IResourceHookExecutor hookExecutor = null) : + base(repository: repository, apiOptions: options, requestManager, resourceGraph, pageManager, loggerFactory, hookExecutor) + { } + + [Obsolete("Dont use this constructor, use the one without JsonApiContext instead")] + public EntityResourceService( + IJsonApiContext context, IEntityRepository repository) : this(repository, context.Options, context.RequestManager, context.PageManager, context.ResourceGraph) { } + } } diff --git a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs index d3fb014bcc..d1a7878c3f 100644 --- a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs @@ -3,8 +3,10 @@ using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Request; @@ -12,7 +14,8 @@ namespace JsonApiDotNetCore.Services { public interface IJsonApiApplication { - JsonApiOptions Options { get; set; } + IJsonApiOptions Options { get; set; } + [Obsolete("Use standalone resourcegraph")] IResourceGraph ResourceGraph { get; set; } } @@ -23,34 +26,10 @@ public interface IQueryRequest PageManager PageManager { get; set; } } - public interface IUpdateRequest - { - /// - /// The attributes that were included in a PATCH request. - /// Only the attributes in this dictionary should be updated. - /// - Dictionary AttributesToUpdate { get; set; } - /// - /// Any relationships that were included in a PATCH request. - /// Only the relationships in this dictionary should be updated. - /// - Dictionary RelationshipsToUpdate { get; } - } - public interface IJsonApiRequest : IJsonApiApplication, IUpdateRequest, IQueryRequest + public interface IJsonApiRequest : IJsonApiApplication, IQueryRequest { - /// - /// The request namespace. This may be an absolute or relative path - /// depending upon the configuration. - /// - /// - /// Absolute: https://example.com/api/v1 - /// - /// Relative: /api/v1 - /// - string BasePath { get; set; } - /// /// Stores information to set relationships for the request resource. /// These relationships must already exist and should not be re-created. @@ -58,7 +37,7 @@ public interface IJsonApiRequest : IJsonApiApplication, IUpdateRequest, IQueryRe /// relationship pointers to persist the relationship. /// /// The expected use case is POST-ing or PATCH-ing an entity with HasMany - /// relaitonships: + /// relationships: /// /// { /// "data": { @@ -143,6 +122,10 @@ public interface IJsonApiRequest : IJsonApiApplication, IUpdateRequest, IQueryRe public interface IJsonApiContext : IJsonApiRequest { + [Obsolete("Use standalone IRequestManager")] + IRequestManager RequestManager { get; set; } + [Obsolete("Use standalone IPageManager")] + IPageManager PageManager { get; set; } IJsonApiContext ApplyContext(object controller); IMetaBuilder MetaBuilder { get; set; } IGenericProcessorFactory GenericProcessorFactory { get; set; } diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index 495354b1e0..435fe98f10 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -4,8 +4,10 @@ using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Internal.Generics; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Request; using Microsoft.AspNetCore.Http; @@ -21,12 +23,16 @@ public class JsonApiContext : IJsonApiContext public JsonApiContext( IResourceGraph resourceGraph, IHttpContextAccessor httpContextAccessor, - JsonApiOptions options, + IJsonApiOptions options, IMetaBuilder metaBuilder, IGenericProcessorFactory genericProcessorFactory, IQueryParser queryParser, + IPageManager pageManager, + IRequestManager requestManager, IControllerContext controllerContext) { + RequestManager = requestManager; + PageManager = pageManager; ResourceGraph = resourceGraph; _httpContextAccessor = httpContextAccessor; Options = options; @@ -36,16 +42,18 @@ public JsonApiContext( _controllerContext = controllerContext; } - public JsonApiOptions Options { get; set; } + public IJsonApiOptions Options { get; set; } + [Obsolete("Please use the standalone `IResourceGraph`")] public IResourceGraph ResourceGraph { get; set; } [Obsolete("Use the proxied member IControllerContext.RequestEntity instead.")] public ContextEntity RequestEntity { get => _controllerContext.RequestEntity; set => _controllerContext.RequestEntity = value; } - public string BasePath { get; set; } + + [Obsolete("Please us the IRequestManager")] public QuerySet QuerySet { get; set; } public bool IsRelationshipData { get; set; } public bool IsRelationshipPath { get; private set; } public List IncludedRelationships { get; set; } - public PageManager PageManager { get; set; } + public IPageManager PageManager { get; set; } public IMetaBuilder MetaBuilder { get; set; } public IGenericProcessorFactory GenericProcessorFactory { get; set; } public Type ControllerType { get; set; } @@ -64,7 +72,11 @@ private Dictionary GetRelationshipsToUpdate() public HasManyRelationshipPointers HasManyRelationshipPointers { get; private set; } = new HasManyRelationshipPointers(); public HasOneRelationshipPointers HasOneRelationshipPointers { get; private set; } = new HasOneRelationshipPointers(); + [Obsolete("Please use the standalone Requestmanager")] + public IRequestManager RequestManager { get; set; } + PageManager IQueryRequest.PageManager { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + [Obsolete("This is no longer necessary")] public IJsonApiContext ApplyContext(object controller) { if (controller == null) @@ -83,8 +95,6 @@ public IJsonApiContext ApplyContext(object controller) IncludedRelationships = QuerySet.IncludedRelationships; } - BasePath = new LinkBuilder(this).GetBasePath(context, _controllerContext.RequestEntity.EntityName); - PageManager = GetPageManager(); IsRelationshipPath = PathIsRelationship(context.Request.Path.Value); return this; @@ -124,20 +134,22 @@ internal static bool PathIsRelationship(string requestPath) return false; } - private PageManager GetPageManager() - { - if (Options.DefaultPageSize == 0 && (QuerySet == null || QuerySet.PageQuery.PageSize == 0)) - return new PageManager(); - - var query = QuerySet?.PageQuery ?? new PageQuery(); - - return new PageManager - { - DefaultPageSize = Options.DefaultPageSize, - CurrentPage = query.PageOffset, - PageSize = query.PageSize > 0 ? query.PageSize : Options.DefaultPageSize - }; - } + //private PageManager GetPageManager() + //{ + // if (Options.DefaultPageSize == 0 && (QuerySet == null || QuerySet.PageQuery.PageSize == 0)) + // { + // return new PageManager(); + // } + + // var query = QuerySet?.PageQuery ?? new PageQuery(); + + // return new PageManager + // { + // DefaultPageSize = Options.DefaultPageSize, + // CurrentPage = query.PageOffset, + // PageSize = query.PageSize > 0 ? query.PageSize : Options.DefaultPageSize + // }; + //} public void BeginOperation() { diff --git a/src/JsonApiDotNetCore/Services/Operations/Processors/CreateOpProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/Processors/CreateOpProcessor.cs index ebc600a5fb..bd4e89eb84 100644 --- a/src/JsonApiDotNetCore/Services/Operations/Processors/CreateOpProcessor.cs +++ b/src/JsonApiDotNetCore/Services/Operations/Processors/CreateOpProcessor.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Models.Operations; using JsonApiDotNetCore.Serialization; diff --git a/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs index 9415554bcb..6535bf21b0 100644 --- a/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs +++ b/src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Models.Operations; using JsonApiDotNetCore.Serialization; diff --git a/src/JsonApiDotNetCore/Services/Operations/Processors/RemoveOpProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/Processors/RemoveOpProcessor.cs index f3efe7939c..70ddae2aea 100644 --- a/src/JsonApiDotNetCore/Services/Operations/Processors/RemoveOpProcessor.cs +++ b/src/JsonApiDotNetCore/Services/Operations/Processors/RemoveOpProcessor.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Models.Operations; using JsonApiDotNetCore.Serialization; diff --git a/src/JsonApiDotNetCore/Services/Operations/Processors/UpdateOpProcessor.cs b/src/JsonApiDotNetCore/Services/Operations/Processors/UpdateOpProcessor.cs index 9b136b30c0..7969b6f3ed 100644 --- a/src/JsonApiDotNetCore/Services/Operations/Processors/UpdateOpProcessor.cs +++ b/src/JsonApiDotNetCore/Services/Operations/Processors/UpdateOpProcessor.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Models.Operations; using JsonApiDotNetCore.Serialization; diff --git a/src/JsonApiDotNetCore/Services/QueryAccessor.cs b/src/JsonApiDotNetCore/Services/QueryAccessor.cs index dc9ff7ef0a..7721f8e85a 100644 --- a/src/JsonApiDotNetCore/Services/QueryAccessor.cs +++ b/src/JsonApiDotNetCore/Services/QueryAccessor.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Managers.Contracts; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Services @@ -17,19 +18,28 @@ public interface IQueryAccessor T GetRequired(string key); } + /// + /// Accessing queries + /// public class QueryAccessor : IQueryAccessor { - private readonly IJsonApiContext _jsonApiContext; + private readonly IRequestManager _requestManager; private readonly ILogger _logger; + /// + /// Creates an instance which can be used to access the qury + /// + /// + /// public QueryAccessor( - IJsonApiContext jsonApiContext, + IRequestManager requestManager, ILogger logger) { - _jsonApiContext = jsonApiContext; + _requestManager = requestManager; _logger = logger; } + public T GetRequired(string key) { if (TryGetValue(key, out T result) == false) @@ -71,13 +81,13 @@ public bool TryGetValue(string key, out T value) } private string GetFilterValue(string key) { - var publicValue = _jsonApiContext.QuerySet.Filters + var publicValue = _requestManager.QuerySet.Filters .FirstOrDefault(f => string.Equals(f.Attribute, key, StringComparison.OrdinalIgnoreCase))?.Value; if(publicValue != null) return publicValue; - var internalValue = _jsonApiContext.QuerySet.Filters + var internalValue = _requestManager.QuerySet.Filters .FirstOrDefault(f => string.Equals(f.Attribute, key, StringComparison.OrdinalIgnoreCase))?.Value; if(internalValue != null) { diff --git a/src/JsonApiDotNetCore/Services/QueryComposer.cs b/src/JsonApiDotNetCore/Services/QueryComposer.cs index e365811704..b96b83718a 100644 --- a/src/JsonApiDotNetCore/Services/QueryComposer.cs +++ b/src/JsonApiDotNetCore/Services/QueryComposer.cs @@ -1,21 +1,22 @@ using System.Collections.Generic; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; namespace JsonApiDotNetCore.Services { public interface IQueryComposer { - string Compose(IJsonApiContext jsonApiContext); + string Compose(IRequestManager jsonApiContext); } public class QueryComposer : IQueryComposer { - public string Compose(IJsonApiContext jsonApiContext) + public string Compose(IRequestManager requestManager) { string result = ""; - if (jsonApiContext != null && jsonApiContext.QuerySet != null) + if (requestManager != null && requestManager.QuerySet != null) { - List filterQueries = jsonApiContext.QuerySet.Filters; + List filterQueries = requestManager.QuerySet.Filters; if (filterQueries.Count > 0) { foreach (FilterQuery filter in filterQueries) diff --git a/src/JsonApiDotNetCore/Services/QueryParser.cs b/src/JsonApiDotNetCore/Services/QueryParser.cs index 5787ab722f..44c6431414 100644 --- a/src/JsonApiDotNetCore/Services/QueryParser.cs +++ b/src/JsonApiDotNetCore/Services/QueryParser.cs @@ -5,6 +5,7 @@ using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using Microsoft.AspNetCore.Http; @@ -17,27 +18,26 @@ public interface IQueryParser public class QueryParser : IQueryParser { - private readonly IControllerContext _controllerContext; - private readonly JsonApiOptions _options; + private readonly IRequestManager _requestManager; + private readonly IJsonApiOptions _options; public QueryParser( - IControllerContext controllerContext, - JsonApiOptions options) + IRequestManager requestManager, + IJsonApiOptions options) { - _controllerContext = controllerContext; + _requestManager = requestManager; _options = options; } public virtual QuerySet Parse(IQueryCollection query) { var querySet = new QuerySet(); - var disabledQueries = _controllerContext.GetControllerAttribute()?.QueryParams ?? QueryParams.None; - + var disabledQueries = _requestManager.DisabledQueryParams; foreach (var pair in query) { if (pair.Key.StartsWith(QueryConstants.FILTER)) { - if (disabledQueries.HasFlag(QueryParams.Filter) == false) + if (disabledQueries.HasFlag(QueryParams.Filters) == false) querySet.Filters.AddRange(ParseFilterQuery(pair.Key, pair.Value)); continue; } @@ -133,9 +133,11 @@ protected virtual PageQuery ParsePageQuery(PageQuery pageQuery, string key, stri const string NUMBER = "number"; if (propertyName == SIZE) + { pageQuery.PageSize = int.TryParse(value, out var pageSize) ? pageSize : throw new JsonApiException(400, $"Invalid page size '{value}'"); + } else if (propertyName == NUMBER) pageQuery.PageOffset = int.TryParse(value, out var pageOffset) ? @@ -184,8 +186,8 @@ protected virtual List ParseFieldsQuery(string key, string value) var typeName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; var includedFields = new List { nameof(Identifiable.Id) }; - var relationship = _controllerContext.RequestEntity.Relationships.SingleOrDefault(a => a.Is(typeName)); - if (relationship == default && string.Equals(typeName, _controllerContext.RequestEntity.EntityName, StringComparison.OrdinalIgnoreCase) == false) + var relationship = _requestManager.GetContextEntity().Relationships.SingleOrDefault(a => a.Is(typeName)); + if (relationship == default && string.Equals(typeName, _requestManager.GetContextEntity().EntityName, StringComparison.OrdinalIgnoreCase) == false) return includedFields; var fields = value.Split(QueryConstants.COMMA); @@ -203,9 +205,9 @@ protected virtual List ParseFieldsQuery(string key, string value) } else { - var attr = _controllerContext.RequestEntity.Attributes.SingleOrDefault(a => a.Is(field)); + var attr = _requestManager.GetContextEntity().Attributes.SingleOrDefault(a => a.Is(field)); if (attr == null) - throw new JsonApiException(400, $"'{_controllerContext.RequestEntity.EntityName}' does not contain '{field}'."); + throw new JsonApiException(400, $"'{_requestManager.GetContextEntity().EntityName}' does not contain '{field}'."); // e.g. "Name" includedFields.Add(attr.InternalAttributeName); @@ -219,14 +221,13 @@ protected virtual AttrAttribute GetAttribute(string propertyName) { try { - return _controllerContext - .RequestEntity + return _requestManager.GetContextEntity() .Attributes .Single(attr => attr.Is(propertyName)); } catch (InvalidOperationException e) { - throw new JsonApiException(400, $"Attribute '{propertyName}' does not exist on resource '{_controllerContext.RequestEntity.EntityName}'", e); + throw new JsonApiException(400, $"Attribute '{propertyName}' does not exist on resource '{_requestManager.GetContextEntity().EntityName}'", e); } } diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 09ffcfcfc1..c20ea49ea5 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -1,8 +1,10 @@ using GettingStarted.Models; using GettingStarted.ResourceDefinitionExample; using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Data; using JsonApiDotNetCore.Graph; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.EntityFrameworkCore; @@ -83,14 +85,14 @@ public class TestModel : Identifiable { } public class TestModelService : EntityResourceService { private static IEntityRepository _repo = new Mock>().Object; - private static IJsonApiContext _jsonApiContext = new Mock().Object; + private static IJsonApiContext _jsonApiContext = new Mock().Object; public TestModelService() : base(_jsonApiContext, _repo) { } } public class TestModelRepository : DefaultEntityRepository { internal static IDbContextResolver _dbContextResolver; - private static IJsonApiContext _jsonApiContext = new Mock().Object; + private static IJsonApiContext _jsonApiContext = new Mock().Object; public TestModelRepository() : base(_jsonApiContext, _dbContextResolver) { } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/CamelCasedModelsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/CamelCasedModelsControllerTests.cs index b2059c10c9..9649bbb2bd 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/CamelCasedModelsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/CamelCasedModelsControllerTests.cs @@ -52,8 +52,7 @@ public async Task Can_Get_CamelCasedModels() // Act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService() - .DeserializeList(body); + var deserializedBody = _fixture.GetService().DeserializeList(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -152,8 +151,7 @@ public async Task Can_Patch_CamelCasedModels() }; var httpMethod = new HttpMethod("PATCH"); var route = $"/camelCasedModels/{model.Id}"; - var builder = new WebHostBuilder() - .UseStartup(); + var builder = new WebHostBuilder().UseStartup(); var server = new TestServer(builder); var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs index 478f40f14f..3b9fd699c7 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs @@ -110,8 +110,7 @@ public async Task CustomRouteControllers_Creates_Proper_Relationship_Links() context.TodoItems.Add(todoItem); await context.SaveChangesAsync(); - var builder = new WebHostBuilder() - .UseStartup(); + var builder = new WebHostBuilder().UseStartup(); var httpMethod = new HttpMethod("GET"); var route = $"/custom/route/todo-items/{todoItem.Id}"; @@ -119,8 +118,10 @@ public async Task CustomRouteControllers_Creates_Proper_Relationship_Links() var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); - // act & assert + // Act var response = await client.SendAsync(request); + + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs index fcc6e5ffde..39fc11178d 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs @@ -27,7 +27,7 @@ public void Can_Return_Custom_Error_Types() }); // act - var result = new JsonApiSerializer(null, null, null) + var result = new JsonApiSerializer(null, null, null,null) .Serialize(errorCollection); // assert diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs index 2030694918..40c74b4500 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs @@ -82,7 +82,7 @@ public async Task CheckNullBehaviorCombination(bool? omitNullValuedAttributes, b { nullAttributeResponseBehavior = new NullAttributeResponseBehavior(); } - var jsonApiOptions = _fixture.GetService(); + var jsonApiOptions = _fixture.GetService(); jsonApiOptions.NullAttributeResponseBehavior = nullAttributeResponseBehavior; jsonApiOptions.AllowCustomQueryParameters = true; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs index 90496b3690..88906ba7ab 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/HttpMethodRestrictions/HttpReadOnlyTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http; using System.Threading.Tasks; using JsonApiDotNetCoreExample; @@ -14,14 +14,14 @@ public class HttpReadOnlyTests [Fact] public async Task Allows_GET_Requests() { - // arrange + // Arrange const string route = "readonly"; const string method = "GET"; - // act + // Act var statusCode = await MakeRequestAsync(route, method); - // assert + // Assert Assert.Equal(HttpStatusCode.OK, statusCode); } @@ -79,4 +79,4 @@ private async Task MakeRequestAsync(string route, string method) return response.StatusCode; } } -} \ No newline at end of file +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs index 93137b4ee2..559a8562d3 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -84,6 +84,7 @@ public async Task Can_Create_Guid_Identifiable_Entity() // act var response = await client.SendAsync(request); + var sdfsd = await response.Content.ReadAsStringAsync(); // assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs index 7ce1604683..dcbb8119ed 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs @@ -44,15 +44,22 @@ public TodoItemControllerTests(TestFixture fixture) } [Fact] - public async Task Can_Get_TodoItems() + public async Task Can_Get_TodoItems_Paginate_Check() { // Arrange - const int expectedEntitiesPerPage = 5; - var person = new Person(); - var todoItem = _todoItemFaker.Generate(); - todoItem.Owner = person; - _context.TodoItems.Add(todoItem); + _context.TodoItems.RemoveRange(_context.TodoItems.ToList()); _context.SaveChanges(); + int expectedEntitiesPerPage = _jsonApiContext.Options.DefaultPageSize; + var person = new Person(); + var todoItems = _todoItemFaker.Generate(expectedEntitiesPerPage +1); + + foreach (var todoItem in todoItems) + { + todoItem.Owner = person; + _context.TodoItems.Add(todoItem); + _context.SaveChanges(); + + } var httpMethod = new HttpMethod("GET"); var route = "/api/v1/todo-items"; @@ -66,7 +73,7 @@ public async Task Can_Get_TodoItems() // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotEmpty(deserializedBody); - Assert.True(deserializedBody.Count <= expectedEntitiesPerPage); + Assert.True(deserializedBody.Count <= expectedEntitiesPerPage, $"There are more items on the page than the default page size. {deserializedBody.Count} > {expectedEntitiesPerPage}"); } [Fact] @@ -96,7 +103,7 @@ public async Task Can_Filter_By_Resource_Id() public async Task Can_Filter_By_Relationship_Id() { // Arrange - var person = new Person(); + var person = new Person(); var todoItem = _todoItemFaker.Generate(); todoItem.Owner = person; _context.TodoItems.Add(todoItem); diff --git a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs index a88ecceb67..d0055a9530 100644 --- a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs +++ b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs @@ -8,6 +8,7 @@ using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; diff --git a/test/UnitTests/Builders/DocumentBuilder_Tests.cs b/test/UnitTests/Builders/DocumentBuilder_Tests.cs index 00b2cad9a0..00b3b57b16 100644 --- a/test/UnitTests/Builders/DocumentBuilder_Tests.cs +++ b/test/UnitTests/Builders/DocumentBuilder_Tests.cs @@ -1,8 +1,11 @@ +using System; using System.Collections; using System.Collections.Generic; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.Extensions.DependencyInjection; @@ -14,7 +17,7 @@ namespace UnitTests public class DocumentBuilder_Tests { private readonly Mock _jsonApiContextMock; - private readonly PageManager _pageManager; + private readonly IPageManager _pageManager; private readonly JsonApiOptions _options; private readonly Mock _requestMetaMock; @@ -43,14 +46,12 @@ public DocumentBuilder_Tests() .Setup(m => m.MetaBuilder) .Returns(new MetaBuilder()); - _pageManager = new PageManager(); + _pageManager = new Mock().Object; _jsonApiContextMock .Setup(m => m.PageManager) .Returns(_pageManager); - _jsonApiContextMock - .Setup(m => m.BasePath) - .Returns("localhost"); + _jsonApiContextMock .Setup(m => m.RequestEntity) @@ -65,7 +66,8 @@ public void Includes_Paging_Links_By_Default() _pageManager.TotalRecords = 1; _pageManager.CurrentPage = 1; - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + + var documentBuilder = GetDocumentBuilder(); var entity = new Model(); // act @@ -76,6 +78,8 @@ public void Includes_Paging_Links_By_Default() Assert.NotNull(document.Links.Last); } + + [Fact] public void Page_Links_Can_Be_Disabled_Globally() { @@ -90,7 +94,7 @@ public void Page_Links_Can_Be_Disabled_Globally() .Setup(m => m.ResourceGraph) .Returns(_options.ResourceGraph); - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + var documentBuilder = GetDocumentBuilder(_jsonApiContextMock); var entity = new Model(); // act @@ -112,7 +116,7 @@ public void Related_Links_Can_Be_Disabled() .Setup(m => m.ResourceGraph) .Returns(_options.ResourceGraph); - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + var documentBuilder = GetDocumentBuilder(_jsonApiContextMock); var entity = new Model(); // act @@ -136,7 +140,7 @@ public void Related_Links_Can_Be_Disabled_Globally() .Setup(m => m.ResourceGraph) .Returns(_options.ResourceGraph); - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + var documentBuilder = GetDocumentBuilder(_jsonApiContextMock); var entity = new RelatedModel(); // act @@ -157,7 +161,7 @@ public void Related_Data_Included_In_Relationships_By_Default() .Setup(m => m.ResourceGraph) .Returns(_options.ResourceGraph); - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + var documentBuilder = GetDocumentBuilder(_jsonApiContextMock); var entity = new Model { RelatedModel = new RelatedModel @@ -189,7 +193,7 @@ public void IndependentIdentifier_Included_In_HasOne_Relationships_By_Default() .Setup(m => m.ResourceGraph) .Returns(_options.ResourceGraph); - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + var documentBuilder = GetDocumentBuilder(_jsonApiContextMock); var entity = new Model { RelatedModelId = relatedId @@ -211,7 +215,7 @@ public void IndependentIdentifier_Included_In_HasOne_Relationships_By_Default() public void Build_Can_Build_Arrays() { var entities = new[] { new Model() }; - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + var documentBuilder = GetDocumentBuilder(_jsonApiContextMock); var documents = documentBuilder.Build(entities); @@ -222,7 +226,7 @@ public void Build_Can_Build_Arrays() public void Build_Can_Build_CustomIEnumerables() { var entities = new Models(new[] { new Model() }); - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object); + var documentBuilder = GetDocumentBuilder(_jsonApiContextMock); var documents = documentBuilder.Build(entities); @@ -247,7 +251,8 @@ public void DocumentBuilderOptions( documentBuilderBehaviourMock.Setup(m => m.GetDocumentBuilderOptions()) .Returns(new DocumentBuilderOptions(omitNullValuedAttributes.Value)); } - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, null, omitNullValuedAttributes.HasValue ? documentBuilderBehaviourMock.Object : null); + var pageManagerMock = new Mock(); + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, pageManagerMock.Object, null, documentBuilderOptionsProvider: omitNullValuedAttributes.HasValue ? documentBuilderBehaviourMock.Object : null); var document = documentBuilder.Build(new Model() { StringProperty = attributeValue }); Assert.Equal(resultContainsAttribute, document.Data.Attributes.ContainsKey("StringProperty")); @@ -303,7 +308,7 @@ public void Build_Will_Use_Resource_If_Defined_For_Multiple_Documents() .AddSingleton(resourceGraph) .BuildServiceProvider()); - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, scopedServiceProvider: scopedServiceProvider); + var documentBuilder = GetDocumentBuilder(scopedServiceProvider: scopedServiceProvider); var documents = documentBuilder.Build(entities); @@ -327,7 +332,7 @@ public void Build_Will_Use_Resource_If_Defined_For_Single_Document() .AddSingleton(resourceGraph) .BuildServiceProvider()); - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, scopedServiceProvider: scopedServiceProvider); + var documentBuilder = GetDocumentBuilder(_jsonApiContextMock, scopedServiceProvider: scopedServiceProvider); var documents = documentBuilder.Build(entity); @@ -350,7 +355,7 @@ public void Build_Will_Use_Instance_Specific_Resource_If_Defined_For_Multiple_Do .AddSingleton(resourceGraph) .BuildServiceProvider()); - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, scopedServiceProvider: scopedServiceProvider); + var documentBuilder = GetDocumentBuilder(_jsonApiContextMock, scopedServiceProvider: scopedServiceProvider); var documents = documentBuilder.Build(entities); @@ -374,10 +379,10 @@ public void Build_Will_Use_Instance_Specific_Resource_If_Defined_For_Single_Docu .AddSingleton(resourceGraph) .BuildServiceProvider()); - var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, scopedServiceProvider: scopedServiceProvider); + var documentBuilder = GetDocumentBuilder(_jsonApiContextMock, scopedServiceProvider: scopedServiceProvider); var documents = documentBuilder.Build(entity); - + Assert.False(documents.Data.Attributes.ContainsKey("password")); Assert.True(documents.Data.Attributes.ContainsKey("username")); } @@ -407,5 +412,20 @@ public UserResource(IResourceGraph graph) : base(graph) protected override List OutputAttrs() => Remove(user => user.Password); } + private DocumentBuilder GetDocumentBuilder(Mock jaContextMock = null, TestScopedServiceProvider scopedServiceProvider = null) + { + var pageManagerMock = new Mock(); + var rmMock = new Mock(); + rmMock.SetupGet(rm => rm.BasePath).Returns("Localhost"); + if (jaContextMock != null) + { + return new DocumentBuilder(_jsonApiContextMock.Object, pageManagerMock.Object, rmMock.Object, scopedServiceProvider: scopedServiceProvider); + + } + else + { + return new DocumentBuilder(jaContextMock.Object, pageManagerMock.Object, rmMock.Object, scopedServiceProvider: scopedServiceProvider); + } + } } } diff --git a/test/UnitTests/Builders/LinkBuilder_Tests.cs b/test/UnitTests/Builders/LinkBuilder_Tests.cs index 69b135de03..1e9fdddda3 100644 --- a/test/UnitTests/Builders/LinkBuilder_Tests.cs +++ b/test/UnitTests/Builders/LinkBuilder_Tests.cs @@ -21,28 +21,29 @@ public void GetBasePath_Returns_Path_Before_Resource(string scheme, string host, string path, bool isRelative, string expectedPath) { // arrange - const string resource = "articles"; - var jsonApiContextMock = new Mock(); - jsonApiContextMock.Setup(m => m.Options).Returns(new JsonApiOptions - { - RelativeLinks = isRelative - }); + //const string resource = "articles"; + //var jsonApiContextMock = new Mock(); + //jsonApiContextMock.Setup(m => m.Options).Returns(new JsonApiOptions + //{ + // RelativeLinks = isRelative + //}); - var requestMock = new Mock(); - requestMock.Setup(m => m.Scheme).Returns(scheme); - requestMock.Setup(m => m.Host).Returns(new HostString(host)); - requestMock.Setup(m => m.Path).Returns(new PathString(path)); + //var requestMock = new Mock(); + //requestMock.Setup(m => m.Scheme).Returns(scheme); + //requestMock.Setup(m => m.Host).Returns(new HostString(host)); + //requestMock.Setup(m => m.Path).Returns(new PathString(path)); - var contextMock = new Mock(); - contextMock.Setup(m => m.Request).Returns(requestMock.Object); + //var contextMock = new Mock(); + //contextMock.Setup(m => m.Request).Returns(requestMock.Object); - var linkBuilder = new LinkBuilder(jsonApiContextMock.Object); + //var linkBuilder = new LinkBuilder(jsonApiContextMock.Object); - // act - var basePath = linkBuilder.GetBasePath(contextMock.Object, resource); + //// act + //var basePath = linkBuilder.GetBasePath(contextMock.Object, resource); - // assert - Assert.Equal(expectedPath, basePath); + //// assert + //Assert.Equal(expectedPath, basePath); + Assert.False(true); } } } diff --git a/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs b/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs index 3397aa4eda..d6c5e951b8 100644 --- a/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs +++ b/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs @@ -8,6 +8,7 @@ using JsonApiDotNetCore.Internal; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using JsonApiDotNetCore.Internal.Contracts; namespace UnitTests { @@ -17,7 +18,7 @@ public class Resource : Identifiable { [Attr("test-attribute")] public string TestAttribute { get; set; } } - private Mock _jsonApiContextMock = new Mock(); + private Mock _resourceGraph = new Mock(); private Mock _resourceGraphMock = new Mock(); [Fact] @@ -25,14 +26,14 @@ public async Task GetAsync_Calls_Service() { // arrange var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getAll: serviceMock.Object); + var controller = new BaseJsonApiController(new Mock().Object, _resourceGraph.Object, getAll: serviceMock.Object); // act await controller.GetAsync(); // assert serviceMock.Verify(m => m.GetAsync(), Times.Once); - VerifyApplyContext(); + } [Fact] @@ -40,7 +41,7 @@ public async Task GetAsync_Throws_405_If_No_Service() { // arrange var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, null); + var controller = new BaseJsonApiController(new Mock().Object, _resourceGraph.Object, null); // act var exception = await Assert.ThrowsAsync(() => controller.GetAsync()); @@ -55,14 +56,14 @@ public async Task GetAsyncById_Calls_Service() // arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getById: serviceMock.Object); + var controller = new BaseJsonApiController(new Mock().Object, _resourceGraph.Object, getById: serviceMock.Object); // act await controller.GetAsync(id); // assert serviceMock.Verify(m => m.GetAsync(id), Times.Once); - VerifyApplyContext(); + } [Fact] @@ -71,7 +72,7 @@ public async Task GetAsyncById_Throws_405_If_No_Service() // arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getById: null); + var controller = new BaseJsonApiController(new Mock().Object, _resourceGraph.Object, getById: null); // act var exception = await Assert.ThrowsAsync(() => controller.GetAsync(id)); @@ -86,14 +87,13 @@ public async Task GetRelationshipsAsync_Calls_Service() // arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationships: serviceMock.Object); + var controller = new BaseJsonApiController(new Mock().Object, _resourceGraph.Object, getRelationships: serviceMock.Object); // act await controller.GetRelationshipsAsync(id, string.Empty); // assert serviceMock.Verify(m => m.GetRelationshipsAsync(id, string.Empty), Times.Once); - VerifyApplyContext(); } [Fact] @@ -102,7 +102,7 @@ public async Task GetRelationshipsAsync_Throws_405_If_No_Service() // arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationships: null); + var controller = new BaseJsonApiController(new Mock().Object, _resourceGraph.Object, getRelationships: null); // act var exception = await Assert.ThrowsAsync(() => controller.GetRelationshipsAsync(id, string.Empty)); @@ -117,14 +117,13 @@ public async Task GetRelationshipAsync_Calls_Service() // arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationship: serviceMock.Object); + var controller = new BaseJsonApiController(new Mock().Object, _resourceGraph.Object, getRelationship: serviceMock.Object); // act await controller.GetRelationshipAsync(id, string.Empty); // assert serviceMock.Verify(m => m.GetRelationshipAsync(id, string.Empty), Times.Once); - VerifyApplyContext(); } [Fact] @@ -133,7 +132,7 @@ public async Task GetRelationshipAsync_Throws_405_If_No_Service() // arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, getRelationship: null); + var controller = new BaseJsonApiController(new Mock().Object, _resourceGraph.Object, getRelationship: null); // act var exception = await Assert.ThrowsAsync(() => controller.GetRelationshipAsync(id, string.Empty)); @@ -149,16 +148,16 @@ public async Task PatchAsync_Calls_Service() const int id = 0; var resource = new Resource(); var serviceMock = new Mock>(); - _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); - _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions()); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: serviceMock.Object); + //_resourceGraph.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_resourceGraph.Object); + + + var controller = new BaseJsonApiController(new JsonApiOptions(), _resourceGraph.Object, update: serviceMock.Object); // act await controller.PatchAsync(id, resource); // assert serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny()), Times.Once); - VerifyApplyContext(); } [Fact] @@ -168,16 +167,15 @@ public async Task PatchAsync_ModelStateInvalid_ValidateModelStateDisbled() const int id = 0; var resource = new Resource(); var serviceMock = new Mock>(); - _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); - _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = false }); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: serviceMock.Object); + //_resourceGraph.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_resourceGraph.Object); + + var controller = new BaseJsonApiController(new JsonApiOptions(), _resourceGraph.Object, update: serviceMock.Object); // act var response = await controller.PatchAsync(id, resource); // assert serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny()), Times.Once); - VerifyApplyContext(); Assert.IsNotType(response); } @@ -188,11 +186,10 @@ public async Task PatchAsync_ModelStateInvalid_ValidateModelStateEnabled() const int id = 0; var resource = new Resource(); var serviceMock = new Mock>(); - _jsonApiContextMock.SetupGet(a => a.ResourceGraph).Returns(_resourceGraphMock.Object); _resourceGraphMock.Setup(a => a.GetPublicAttributeName("TestAttribute")).Returns("test-attribute"); - _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); - _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions{ValidateModelState = true}); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: serviceMock.Object); +// _resourceGraph.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_resourceGraph.Object); + + var controller = new BaseJsonApiController(new JsonApiOptions { ValidateModelState = true }, _resourceGraph.Object, update: serviceMock.Object); controller.ModelState.AddModelError("TestAttribute", "Failed Validation"); // act @@ -210,7 +207,7 @@ public async Task PatchAsync_Throws_405_If_No_Service() // arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: null); + var controller = new BaseJsonApiController(new Mock().Object, _resourceGraph.Object, update: null); // act var exception = await Assert.ThrowsAsync(() => controller.PatchAsync(id, It.IsAny())); @@ -225,9 +222,9 @@ public async Task PostAsync_Calls_Service() // arrange var resource = new Resource(); var serviceMock = new Mock>(); - _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); - _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions()); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, create: serviceMock.Object); +// _resourceGraph.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_resourceGraph.Object); + + var controller = new BaseJsonApiController(new JsonApiOptions(), _resourceGraph.Object, create: serviceMock.Object); serviceMock.Setup(m => m.CreateAsync(It.IsAny())).ReturnsAsync(resource); controller.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext {HttpContext = new DefaultHttpContext()}; @@ -236,7 +233,6 @@ public async Task PostAsync_Calls_Service() // assert serviceMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Once); - VerifyApplyContext(); } [Fact] @@ -245,9 +241,9 @@ public async Task PostAsync_ModelStateInvalid_ValidateModelStateDisabled() // arrange var resource = new Resource(); var serviceMock = new Mock>(); - _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); - _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = false }); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, create: serviceMock.Object); + //_resourceGraph.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_resourceGraph.Object); + //_resourceGraph.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = false }); + var controller = new BaseJsonApiController(new JsonApiOptions { ValidateModelState = false }, _resourceGraph.Object, create: serviceMock.Object); serviceMock.Setup(m => m.CreateAsync(It.IsAny())).ReturnsAsync(resource); controller.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext { HttpContext = new DefaultHttpContext() }; @@ -256,7 +252,6 @@ public async Task PostAsync_ModelStateInvalid_ValidateModelStateDisabled() // assert serviceMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Once); - VerifyApplyContext(); Assert.IsNotType(response); } @@ -266,11 +261,11 @@ public async Task PostAsync_ModelStateInvalid_ValidateModelStateEnabled() // arrange var resource = new Resource(); var serviceMock = new Mock>(); - _jsonApiContextMock.SetupGet(a => a.ResourceGraph).Returns(_resourceGraphMock.Object); + //_resourceGraph.SetupGet(a => a.ResourceGraph).Returns(_resourceGraphMock.Object); _resourceGraphMock.Setup(a => a.GetPublicAttributeName("TestAttribute")).Returns("test-attribute"); - _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object); - _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = true }); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, create: serviceMock.Object); + //_resourceGraph.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_resourceGraph.Object); + //_resourceGraph.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = true }); + var controller = new BaseJsonApiController(new JsonApiOptions { ValidateModelState = false }, _resourceGraph.Object, create: serviceMock.Object); controller.ModelState.AddModelError("TestAttribute", "Failed Validation"); // act @@ -289,14 +284,13 @@ public async Task PatchRelationshipsAsync_Calls_Service() const int id = 0; var resource = new Resource(); var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, updateRelationships: serviceMock.Object); + var controller = new BaseJsonApiController(new Mock().Object, _resourceGraph.Object, updateRelationships: serviceMock.Object); // act await controller.PatchRelationshipsAsync(id, string.Empty, null); // assert serviceMock.Verify(m => m.UpdateRelationshipsAsync(id, string.Empty, null), Times.Once); - VerifyApplyContext(); } [Fact] @@ -305,7 +299,7 @@ public async Task PatchRelationshipsAsync_Throws_405_If_No_Service() // arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, updateRelationships: null); + var controller = new BaseJsonApiController(new Mock().Object, _resourceGraph.Object, updateRelationships: null); // act var exception = await Assert.ThrowsAsync(() => controller.PatchRelationshipsAsync(id, string.Empty, null)); @@ -317,18 +311,17 @@ public async Task PatchRelationshipsAsync_Throws_405_If_No_Service() [Fact] public async Task DeleteAsync_Calls_Service() { - // arrange + // Arrange const int id = 0; var resource = new Resource(); var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, delete: serviceMock.Object); + var controller = new BaseJsonApiController(new Mock().Object, _resourceGraph.Object, delete: serviceMock.Object); - // act + // Act await controller.DeleteAsync(id); - // assert + // Assert serviceMock.Verify(m => m.DeleteAsync(id), Times.Once); - VerifyApplyContext(); } [Fact] @@ -337,7 +330,9 @@ public async Task DeleteAsync_Throws_405_If_No_Service() // arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new BaseJsonApiController(_jsonApiContextMock.Object, delete: null); + var controller = new BaseJsonApiController(new Mock().Object, + + _resourceGraph.Object, delete: null); // act var exception = await Assert.ThrowsAsync(() => controller.DeleteAsync(id)); @@ -346,7 +341,6 @@ public async Task DeleteAsync_Throws_405_If_No_Service() Assert.Equal(405, exception.GetStatusCode()); } - private void VerifyApplyContext() - => _jsonApiContextMock.Verify(m => m.ApplyContext(It.IsAny>()), Times.Once); + } } diff --git a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index 3b49dda9bf..41d546faf6 100644 --- a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using JsonApiDotNetCore.Internal.Contracts; namespace UnitTests.Extensions { diff --git a/test/UnitTests/JsonApiContext/BasicTest.cs b/test/UnitTests/JsonApiContext/BasicTest.cs new file mode 100644 index 0000000000..7dd134ea97 --- /dev/null +++ b/test/UnitTests/JsonApiContext/BasicTest.cs @@ -0,0 +1,102 @@ +using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreExample.Controllers; +using JsonApiDotNetCoreExample.Models; +using Moq; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Microsoft.AspNetCore.Mvc; +using JsonApiDotNetCoreExample.Services; +using JsonApiDotNetCore.Data; +using Microsoft.Extensions.Logging; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal; +using System.Net; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Internal.Query; +using System.Linq; + +namespace UnitTests.Services +{ + public class EntityResourceServiceMore + { + [Fact] + public async Task TestCanGetAll() + { + + } + + /// + /// we expect the service layer to give use a 404 if there is no entity returned + /// + /// + [Fact] + public async Task GetAsync_Throw404OnNoEntityFound() + { + // Arrange + var jacMock = FetchContextMock(); + var loggerMock = new Mock(); + var jsonApiOptions = new JsonApiOptions + { + IncludeTotalRecordCount = false + } as IJsonApiOptions; + var repositoryMock = new Mock>(); + var queryManagerMock = new Mock(); + var pageManagerMock = new Mock(); + var rgMock = new Mock(); + var service = new CustomArticleService(repositoryMock.Object, jsonApiOptions, queryManagerMock.Object, pageManagerMock.Object, rgMock.Object); + + // Act / Assert + var toExecute = new Func(() => + { + return service.GetAsync(4); + }); + var exception = await Assert.ThrowsAsync(toExecute); + Assert.Equal(404, exception.GetStatusCode()); + } + + /// + /// we expect the service layer to give use a 404 if there is no entity returned + /// + /// + [Fact] + public async Task GetAsync_ShouldThrow404OnNoEntityFoundWithRelationships() + { + // Arrange + var jacMock = FetchContextMock(); + var loggerMock = new Mock(); + var jsonApiOptions = new JsonApiOptions + { + IncludeTotalRecordCount = false + } as IJsonApiOptions; + var repositoryMock = new Mock>(); + + var requestManager = new Mock(); + var pageManagerMock = new Mock(); + requestManager.Setup(qm => qm.GetRelationships()).Returns(new List() { "cookies" }); + requestManager.SetupGet(rm => rm.QuerySet).Returns(new QuerySet + { + IncludedRelationships = new List { "cookies" } + }); + var rgMock = new Mock(); + var service = new CustomArticleService(repositoryMock.Object, jsonApiOptions, requestManager.Object, pageManagerMock.Object, rgMock.Object); + + // Act / Assert + var toExecute = new Func(() => + { + return service.GetAsync(4); + }); + var exception = await Assert.ThrowsAsync(toExecute); + Assert.Equal(404, exception.GetStatusCode()); + } + + public Mock FetchContextMock() + { + return new Mock(); + } + + } +} diff --git a/test/UnitTests/Models/ResourceDefinitionTests.cs b/test/UnitTests/Models/ResourceDefinitionTests.cs index 5abba48ca8..5885808407 100644 --- a/test/UnitTests/Models/ResourceDefinitionTests.cs +++ b/test/UnitTests/Models/ResourceDefinitionTests.cs @@ -1,5 +1,6 @@ using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Models; using System.Collections.Generic; diff --git a/test/UnitTests/ResourceHooks/DiscoveryTests.cs b/test/UnitTests/ResourceHooks/DiscoveryTests.cs index bde024f530..c3467c525d 100644 --- a/test/UnitTests/ResourceHooks/DiscoveryTests.cs +++ b/test/UnitTests/ResourceHooks/DiscoveryTests.cs @@ -4,6 +4,7 @@ using Xunit; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; namespace UnitTests.ResourceHooks { diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/AfterDeleteTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/AfterDeleteTests.cs index 051fea3bba..edc0f6e4ae 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/AfterDeleteTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Delete/AfterDeleteTests.cs @@ -13,15 +13,15 @@ public class AfterDeleteTests : HooksTestsSetup [Fact] public void AfterDelete() { - // arrange + // Arrange var discovery = SetDiscoverableHooks(targetHooks, DisableDbValues); - (var contextMock, var hookExecutor, var resourceDefinitionMock) = CreateTestObjects(discovery); + var (contextMock, hookExecutor, resourceDefinitionMock) = CreateTestObjects(discovery); var todoList = CreateTodoWithOwner(); - // act + // Act hookExecutor.AfterDelete(todoList, ResourcePipeline.Delete, It.IsAny()); - // assert + // Assert resourceDefinitionMock.Verify(rd => rd.AfterDelete(It.IsAny>(), ResourcePipeline.Delete, It.IsAny()), Times.Once()); VerifyNoOtherCalls(resourceDefinitionMock); } diff --git a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs index 17065d3bec..846ecd659e 100644 --- a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs +++ b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs @@ -15,6 +15,8 @@ using System.Collections.Generic; using System.Linq; using Person = JsonApiDotNetCoreExample.Models.Person; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; namespace UnitTests.ResourceHooks { @@ -134,45 +136,54 @@ protected List CreateTodoWithOwner() public class HooksTestsSetup : HooksDummyData { - protected (Mock, IResourceHookExecutor, Mock>) - CreateTestObjects(IHooksDiscovery discovery = null) - where TMain : class, IIdentifiable + (Mock, Mock, Mock, Mock) CreateMocks() + { + var pfMock = new Mock(); + var rgMock = new Mock(); + var rqMock = new Mock(); + var optionsMock = new Mock(new JsonApiOptions { LoadDatabaseValues = false }); + return (rgMock, rqMock, pfMock, optionsMock); + } + + internal (ResourceHookExecutor, Mock>) CreateTestObjects(IHooksDiscovery discovery = null) + where TMain : class, IIdentifiable { // creates the resource definition mock and corresponding ImplementedHooks discovery instance var mainResource = CreateResourceDefinition(discovery); // mocking the GenericProcessorFactory and JsonApiContext and wiring them up. - (var context, var processorFactory) = CreateContextAndProcessorMocks(); + var (rgMock, rqMock, gpfMock, optionsMock) = CreateMocks(); + + var traversalHelper = new TraversalHelper(rgMock.Object, rqMock.Object); - // wiring up the mocked GenericProcessorFactory to return the correct resource definition - SetupProcessorFactoryForResourceDefinition(processorFactory, mainResource.Object, discovery, context.Object); - var meta = new HookExecutorHelper(context.Object.GenericProcessorFactory, ResourceGraph.Instance, context.Object); - var hookExecutor = new ResourceHookExecutor(meta, context.Object, ResourceGraph.Instance); + var meta = new HookExecutorHelper(gpfMock.Object, rgMock.Object, optionsMock.Object); + var hookExecutor = new ResourceHookExecutor(meta, traversalHelper, rgMock.Object, rqMock.Object); - return (context, hookExecutor, mainResource); + return (hookExecutor, mainResource); } protected (Mock context, IResourceHookExecutor, Mock>, Mock>) CreateTestObjects( - IHooksDiscovery mainDiscovery = null, - IHooksDiscovery nestedDiscovery = null, - DbContextOptions repoDbContextOptions = null + IHooksDiscovery mainDiscovery = null, + IHooksDiscovery nestedDiscovery = null, + DbContextOptions repoDbContextOptions = null ) - where TMain : class, IIdentifiable - where TNested : class, IIdentifiable + where TMain : class, IIdentifiable + where TNested : class, IIdentifiable { // creates the resource definition mock and corresponding for a given set of discoverable hooks var mainResource = CreateResourceDefinition(mainDiscovery); var nestedResource = CreateResourceDefinition(nestedDiscovery); // mocking the GenericProcessorFactory and JsonApiContext and wiring them up. - (var context, var processorFactory) = CreateContextAndProcessorMocks(); + var (rgMock, rqMock, gpfMock, optionsMock) = CreateMocks(); var dbContext = repoDbContextOptions != null ? new AppDbContext(repoDbContextOptions) : null; + var traversalHelper = new TraversalHelper(rgMock.Object, rqMock.Object); - SetupProcessorFactoryForResourceDefinition(processorFactory, mainResource.Object, mainDiscovery, context.Object, dbContext); - var meta = new HookExecutorHelper(context.Object.GenericProcessorFactory, ResourceGraph.Instance, context.Object); - var hookExecutor = new ResourceHookExecutor(meta, context.Object, ResourceGraph.Instance); + SetupProcessorFactoryForResourceDefinition(gpfMock.Object, mainResource.Object, mainDiscovery, context.Object, dbContext); + var meta = new HookExecutorHelper(context.Object.GenericProcessorFactory, ResourceGraph.Instance, context.Object.Options); + var hookExecutor = new ResourceHookExecutor(meta, traversalHelper, ResourceGraph.Instance, context.Object.RequestManager); SetupProcessorFactoryForResourceDefinition(processorFactory, nestedResource.Object, nestedDiscovery, context.Object, dbContext); @@ -181,10 +192,10 @@ public class HooksTestsSetup : HooksDummyData protected (Mock context, IResourceHookExecutor, Mock>, Mock>, Mock>) CreateTestObjects( - IHooksDiscovery mainDiscovery = null, - IHooksDiscovery firstNestedDiscovery = null, - IHooksDiscovery secondNestedDiscovery = null, - DbContextOptions repoDbContextOptions = null + IHooksDiscovery mainDiscovery = null, + IHooksDiscovery firstNestedDiscovery = null, + IHooksDiscovery secondNestedDiscovery = null, + DbContextOptions repoDbContextOptions = null ) where TMain : class, IIdentifiable where TFirstNested : class, IIdentifiable @@ -196,13 +207,14 @@ public class HooksTestsSetup : HooksDummyData var secondNestedResource = CreateResourceDefinition(secondNestedDiscovery); // mocking the GenericProcessorFactory and JsonApiContext and wiring them up. - (var context, var processorFactory) = CreateContextAndProcessorMocks(); + (var context, var processorFactory) = CreateMocks(); + var traversalHelper = new TraversalHelper(ResourceGraph.Instance, context.Object); var dbContext = repoDbContextOptions != null ? new AppDbContext(repoDbContextOptions) : null; SetupProcessorFactoryForResourceDefinition(processorFactory, mainResource.Object, mainDiscovery, context.Object, dbContext); - var meta = new HookExecutorHelper(context.Object.GenericProcessorFactory, ResourceGraph.Instance, context.Object); - var hookExecutor = new ResourceHookExecutor(meta, context.Object, ResourceGraph.Instance); + var hookExecutorHelper = new HookExecutorHelper(context.Object.GenericProcessorFactory, ResourceGraph.Instance, context.Object.Options); + var hookExecutor = new ResourceHookExecutor(hookExecutorHelper, traversalHelper, ResourceGraph.Instance, context.Object.RequestManager); SetupProcessorFactoryForResourceDefinition(processorFactory, firstNestedResource.Object, firstNestedDiscovery, context.Object, dbContext); SetupProcessorFactoryForResourceDefinition(processorFactory, secondNestedResource.Object, secondNestedDiscovery, context.Object, dbContext); @@ -239,8 +251,8 @@ protected void VerifyNoOtherCalls(params dynamic[] resourceMocks) protected DbContextOptions InitInMemoryDb(Action seeder) { var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: "repository_mock") - .Options; + .UseInMemoryDatabase(databaseName: "repository_mock") + .Options; using (var context = new AppDbContext(options)) { @@ -253,70 +265,59 @@ protected DbContextOptions InitInMemoryDb(Action seeder void MockHooks(Mock> resourceDefinition) where TModel : class, IIdentifiable { resourceDefinition - .Setup(rd => rd.BeforeCreate(It.IsAny>(), It.IsAny())) - .Returns, ResourcePipeline>((entities, context) => entities) - .Verifiable(); + .Setup(rd => rd.BeforeCreate(It.IsAny>(), It.IsAny())) + .Returns, ResourcePipeline>((entities, context) => entities) + .Verifiable(); resourceDefinition - .Setup(rd => rd.BeforeRead(It.IsAny(), It.IsAny(), It.IsAny())) - .Verifiable(); + .Setup(rd => rd.BeforeRead(It.IsAny(), It.IsAny(), It.IsAny())) + .Verifiable(); resourceDefinition - .Setup(rd => rd.BeforeUpdate(It.IsAny>(), It.IsAny())) - .Returns, ResourcePipeline>((entityDiff, context) => entityDiff.Entities) - .Verifiable(); + .Setup(rd => rd.BeforeUpdate(It.IsAny>(), It.IsAny())) + .Returns, ResourcePipeline>((entityDiff, context) => entityDiff.Entities) + .Verifiable(); resourceDefinition - .Setup(rd => rd.BeforeDelete(It.IsAny>(), It.IsAny())) - .Returns, ResourcePipeline>((entities, context) => entities) - .Verifiable(); + .Setup(rd => rd.BeforeDelete(It.IsAny>(), It.IsAny())) + .Returns, ResourcePipeline>((entities, context) => entities) + .Verifiable(); resourceDefinition - .Setup(rd => rd.BeforeUpdateRelationship(It.IsAny>(), It.IsAny>(), It.IsAny())) - .Returns, IRelationshipsDictionary, ResourcePipeline>((ids, context, helper) => ids) - .Verifiable(); + .Setup(rd => rd.BeforeUpdateRelationship(It.IsAny>(), It.IsAny>(), It.IsAny())) + .Returns, IRelationshipsDictionary, ResourcePipeline>((ids, context, helper) => ids) + .Verifiable(); resourceDefinition - .Setup(rd => rd.BeforeImplicitUpdateRelationship(It.IsAny>(), It.IsAny())) - .Verifiable(); + .Setup(rd => rd.BeforeImplicitUpdateRelationship(It.IsAny>(), It.IsAny())) + .Verifiable(); resourceDefinition - .Setup(rd => rd.OnReturn(It.IsAny>(), It.IsAny())) - .Returns, ResourcePipeline>((entities, context) => entities) - .Verifiable(); + .Setup(rd => rd.OnReturn(It.IsAny>(), It.IsAny())) + .Returns, ResourcePipeline>((entities, context) => entities) + .Verifiable(); resourceDefinition - .Setup(rd => rd.AfterCreate(It.IsAny>(), It.IsAny())) - .Verifiable(); + .Setup(rd => rd.AfterCreate(It.IsAny>(), It.IsAny())) + .Verifiable(); resourceDefinition - .Setup(rd => rd.AfterRead(It.IsAny>(), It.IsAny(), It.IsAny())) - .Verifiable(); + .Setup(rd => rd.AfterRead(It.IsAny>(), It.IsAny(), It.IsAny())) + .Verifiable(); resourceDefinition - .Setup(rd => rd.AfterUpdate(It.IsAny>(), It.IsAny())) - .Verifiable(); + .Setup(rd => rd.AfterUpdate(It.IsAny>(), It.IsAny())) + .Verifiable(); resourceDefinition - .Setup(rd => rd.AfterDelete(It.IsAny>(), It.IsAny(), It.IsAny())) - .Verifiable(); + .Setup(rd => rd.AfterDelete(It.IsAny>(), It.IsAny(), It.IsAny())) + .Verifiable(); } - (Mock, Mock) CreateContextAndProcessorMocks() - { - var processorFactory = new Mock(); - var context = new Mock(); - context.Setup(c => c.GenericProcessorFactory).Returns(processorFactory.Object); - context.Setup(c => c.Options).Returns(new JsonApiOptions { LoadDatabaseValues = false }); - context.Setup(c => c.ResourceGraph).Returns(ResourceGraph.Instance); - return (context, processorFactory); - } void SetupProcessorFactoryForResourceDefinition( - Mock processorFactory, - IResourceHookContainer modelResource, - IHooksDiscovery discovery, - IJsonApiContext apiContext, - AppDbContext dbContext = null + Mock processorFactory, + IResourceHookContainer modelResource, + IHooksDiscovery discovery, + IJsonApiContext apiContext, + AppDbContext dbContext = null ) where TModel : class, IIdentifiable { - processorFactory.Setup(c => c.GetProcessor(typeof(ResourceDefinition<>), typeof(TModel))) - .Returns(modelResource); + processorFactory.Setup(c => c.GetProcessor(typeof(ResourceDefinition<>), typeof(TModel))).Returns(modelResource); - processorFactory.Setup(c => c.GetProcessor(typeof(IHooksDiscovery<>), typeof(TModel))) - .Returns(discovery); + processorFactory.Setup(c => c.GetProcessor(typeof(IHooksDiscovery<>), typeof(TModel))).Returns(discovery); if (dbContext != null) { @@ -325,7 +326,8 @@ void SetupProcessorFactoryForResourceDefinition( { IEntityReadRepository repo = CreateTestRepository(dbContext, apiContext); processorFactory.Setup(c => c.GetProcessor>(typeof(IEntityReadRepository<,>), typeof(TModel), typeof(int))).Returns(repo); - } else + } + else { throw new TypeLoadException("Test not set up properly"); } diff --git a/test/UnitTests/Serialization/JsonApiSerializerTests.cs b/test/UnitTests/Serialization/JsonApiSerializerTests.cs index c781617d03..fc3b716bf3 100644 --- a/test/UnitTests/Serialization/JsonApiSerializerTests.cs +++ b/test/UnitTests/Serialization/JsonApiSerializerTests.cs @@ -1,10 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Request; using JsonApiDotNetCore.Serialization; @@ -23,7 +24,7 @@ public void Can_Serialize_Complex_Types() // arrange var resourceGraphBuilder = new ResourceGraphBuilder(); resourceGraphBuilder.AddResource("test-resource"); - + var serializer = GetSerializer(resourceGraphBuilder); var resource = new TestResource @@ -40,7 +41,7 @@ public void Can_Serialize_Complex_Types() // assert Assert.NotNull(result); - var expectedFormatted = + var expectedFormatted = @"{ ""data"": { ""attributes"": { @@ -61,7 +62,7 @@ public void Can_Serialize_Complex_Types() } }"; var expected = Regex.Replace(expectedFormatted, @"\s+", ""); - + Assert.Equal(expected, result); } @@ -73,7 +74,7 @@ public void Can_Serialize_Deeply_Nested_Relationships() resourceGraphBuilder.AddResource("test-resource"); resourceGraphBuilder.AddResource("children"); resourceGraphBuilder.AddResource("infections"); - + var serializer = GetSerializer( resourceGraphBuilder, included: new List { "children.infections" } @@ -101,8 +102,8 @@ public void Can_Serialize_Deeply_Nested_Relationships() // assert Assert.NotNull(result); - - var expectedFormatted = + + var expectedFormatted = @"{ ""data"": { ""attributes"": { @@ -206,7 +207,7 @@ public void Can_Serialize_Deeply_Nested_Relationships() } private JsonApiSerializer GetSerializer( - ResourceGraphBuilder resourceGraphBuilder, + ResourceGraphBuilder resourceGraphBuilder, List included = null) { var resourceGraph = resourceGraphBuilder.Build(); @@ -221,10 +222,10 @@ private JsonApiSerializer GetSerializer( // jsonApiContextMock.Setup(m => m.HasManyRelationshipPointers).Returns(new HasManyRelationshipPointers()); // jsonApiContextMock.Setup(m => m.HasOneRelationshipPointers).Returns(new HasOneRelationshipPointers()); jsonApiContextMock.Setup(m => m.MetaBuilder).Returns(new MetaBuilder()); - jsonApiContextMock.Setup(m => m.PageManager).Returns(new PageManager()); + var pmMock = new Mock(); + jsonApiContextMock.Setup(m => m.PageManager).Returns(pmMock.Object); + - if (included != null) - jsonApiContextMock.Setup(m => m.IncludedRelationships).Returns(included); var jsonApiOptions = new JsonApiOptions(); jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); @@ -239,7 +240,7 @@ private JsonApiSerializer GetSerializer( var provider = services.BuildServiceProvider(); var scoped = new TestScopedServiceProvider(provider); - var documentBuilder = new DocumentBuilder(jsonApiContextMock.Object, scopedServiceProvider: scoped); + var documentBuilder = GetDocumentBuilder(jsonApiContextMock, scopedServiceProvider: scoped); var serializer = new JsonApiSerializer(jsonApiContextMock.Object, documentBuilder); return serializer; @@ -260,7 +261,7 @@ private class ComplexType private class ChildResource : Identifiable { - [HasMany("infections")] public List Infections { get; set;} + [HasMany("infections")] public List Infections { get; set; } [HasOne("parent")] public TestResource Parent { get; set; } } @@ -269,5 +270,14 @@ private class InfectionResource : Identifiable { [HasOne("infected")] public ChildResource Infected { get; set; } } + + private DocumentBuilder GetDocumentBuilder(Mock jaContextMock, TestScopedServiceProvider scopedServiceProvider = null) + { + var pageManagerMock = new Mock(); + var rmMock = new Mock(); + + return new DocumentBuilder(jaContextMock.Object, pageManagerMock.Object, rmMock.Object, scopedServiceProvider: scopedServiceProvider); + + } } } diff --git a/test/UnitTests/Services/EntityResourceService_Tests.cs b/test/UnitTests/Services/EntityResourceService_Tests.cs index 0643110b21..f7a3b59dfd 100644 --- a/test/UnitTests/Services/EntityResourceService_Tests.cs +++ b/test/UnitTests/Services/EntityResourceService_Tests.cs @@ -72,7 +72,9 @@ public async Task GetRelationshipAsync_Returns_Relationship_Value() Assert.Equal(todoItem.Collection.Id, collection.Id); } - private EntityResourceService GetService() => - new EntityResourceService(_jsonApiContextMock.Object, _repositoryMock.Object, _loggerFactory, null); + private EntityResourceService GetService() + { + return new EntityResourceService(_repositoryMock.Object,_jsonApiContextMock.Object.Options, _jsonApiContextMock.Object.RequestManager, _jsonApiContextMock.Object.PageManager, _jsonApiContextMock.Object.ResourceGraph, _loggerFactory, null); + } } } diff --git a/test/UnitTests/Services/QueryAccessorTests.cs b/test/UnitTests/Services/QueryAccessorTests.cs index aa8bc6ae7e..7c23addd56 100644 --- a/test/UnitTests/Services/QueryAccessorTests.cs +++ b/test/UnitTests/Services/QueryAccessorTests.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Collections.Generic; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -12,21 +13,21 @@ namespace UnitTests.Services { public class QueryAccessorTests { - private readonly Mock _contextMock; + private readonly Mock _rmMock; private readonly Mock> _loggerMock; private readonly Mock _queryMock; public QueryAccessorTests() { - _contextMock = new Mock(); + _rmMock = new Mock(); _loggerMock = new Mock>(); _queryMock = new Mock(); } [Fact] - public void Can_Get_Guid_QueryValue() + public void GetGuid_GuidIsValid_IsReturnedCorrectly() { - // arrange + // Arrange const string key = "SomeId"; var value = Guid.NewGuid(); var querySet = new QuerySet @@ -36,9 +37,9 @@ public void Can_Get_Guid_QueryValue() } }; - _contextMock.Setup(c => c.QuerySet).Returns(querySet); + _rmMock.Setup(c => c.QuerySet).Returns(querySet); - var service = new QueryAccessor(_contextMock.Object, _loggerMock.Object); + var service = new QueryAccessor(_rmMock.Object, _loggerMock.Object); // act var success = service.TryGetValue("SomeId", out Guid result); @@ -51,7 +52,7 @@ public void Can_Get_Guid_QueryValue() [Fact] public void GetRequired_Throws_If_Not_Present() { - // arrange + // Arrange const string key = "SomeId"; var value = Guid.NewGuid(); @@ -62,9 +63,9 @@ public void GetRequired_Throws_If_Not_Present() } }; - _contextMock.Setup(c => c.QuerySet).Returns(querySet); + _rmMock.Setup(c => c.QuerySet).Returns(querySet); - var service = new QueryAccessor(_contextMock.Object, _loggerMock.Object); + var service = new QueryAccessor(_rmMock.Object, _loggerMock.Object); // act var exception = Assert.Throws(() => service.GetRequired("Invalid")); @@ -87,14 +88,14 @@ public void GetRequired_Does_Not_Throw_If_Present() } }; - _contextMock.Setup(c => c.QuerySet).Returns(querySet); + _rmMock.Setup(c => c.QuerySet).Returns(querySet); - var service = new QueryAccessor(_contextMock.Object, _loggerMock.Object); + var service = new QueryAccessor(_rmMock.Object, _loggerMock.Object); - // act + // Act var result = service.GetRequired("SomeId"); - // assert + // Assert Assert.Equal(value, result); } } diff --git a/test/UnitTests/Services/QueryComposerTests.cs b/test/UnitTests/Services/QueryComposerTests.cs index efa600f2f3..607c321b6c 100644 --- a/test/UnitTests/Services/QueryComposerTests.cs +++ b/test/UnitTests/Services/QueryComposerTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Services; using Moq; using Xunit; @@ -25,13 +26,14 @@ public void Can_ComposeEqual_FilterStringForUrl() filters.Add(filter); querySet.Filters = filters; - _jsonApiContext + var rmMock = new Mock(); + rmMock .Setup(m => m.QuerySet) .Returns(querySet); var queryComposer = new QueryComposer(); // act - var filterString = queryComposer.Compose(_jsonApiContext.Object); + var filterString = queryComposer.Compose(rmMock.Object); // assert Assert.Equal("&filter[attribute]=eq:value", filterString); } @@ -47,14 +49,15 @@ public void Can_ComposeLessThan_FilterStringForUrl() filters.Add(filter); filters.Add(filter2); querySet.Filters = filters; - - _jsonApiContext + var rmMock = new Mock(); + rmMock .Setup(m => m.QuerySet) .Returns(querySet); + var queryComposer = new QueryComposer(); // act - var filterString = queryComposer.Compose(_jsonApiContext.Object); + var filterString = queryComposer.Compose(rmMock.Object); // assert Assert.Equal("&filter[attribute]=le:value&filter[attribute2]=value2", filterString); } @@ -65,13 +68,15 @@ public void NoFilter_Compose_EmptyStringReturned() // arrange var querySet = new QuerySet(); - _jsonApiContext + var rmMock = new Mock(); + rmMock .Setup(m => m.QuerySet) .Returns(querySet); var queryComposer = new QueryComposer(); - // act - var filterString = queryComposer.Compose(_jsonApiContext.Object); + // Act + + var filterString = queryComposer.Compose(rmMock.Object); // assert Assert.Equal("", filterString); } diff --git a/test/UnitTests/Services/QueryParser_Tests.cs b/test/UnitTests/Services/QueryParser_Tests.cs index c0e5752dad..994718e188 100644 --- a/test/UnitTests/Services/QueryParser_Tests.cs +++ b/test/UnitTests/Services/QueryParser_Tests.cs @@ -3,6 +3,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; @@ -14,12 +15,12 @@ namespace UnitTests.Services { public class QueryParser_Tests { - private readonly Mock _controllerContextMock; + private readonly Mock _requestMock; private readonly Mock _queryCollectionMock; public QueryParser_Tests() { - _controllerContextMock = new Mock(); + _requestMock = new Mock(); _queryCollectionMock = new Mock(); } @@ -35,11 +36,11 @@ public void Can_Build_Filters() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.None)); + _requestMock + .Setup(m => m.DisabledQueryParams) + .Returns(QueryParams.None); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_requestMock.Object, new JsonApiOptions()); // act var querySet = queryParser.Parse(_queryCollectionMock.Object); @@ -61,11 +62,11 @@ public void Filters_Properly_Parses_DateTime_With_Operation() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.None)); + _requestMock + .Setup(m => m.DisabledQueryParams) + .Returns(QueryParams.None); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_requestMock.Object, new JsonApiOptions()); // act var querySet = queryParser.Parse(_queryCollectionMock.Object); @@ -88,11 +89,11 @@ public void Filters_Properly_Parses_DateTime_Without_Operation() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.None)); + _requestMock + .Setup(m => m.DisabledQueryParams) + .Returns(QueryParams.None); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_requestMock.Object, new JsonApiOptions()); // act var querySet = queryParser.Parse(_queryCollectionMock.Object); @@ -105,7 +106,7 @@ public void Filters_Properly_Parses_DateTime_Without_Operation() [Fact] public void Can_Disable_Filters() { - // arrange + // Arrange var query = new Dictionary { { "filter[key]", new StringValues("value") } }; @@ -114,16 +115,16 @@ public void Can_Disable_Filters() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.Filter)); + _requestMock + .Setup(m => m.DisabledQueryParams) + .Returns(QueryParams.Filters); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_requestMock.Object, new JsonApiOptions()); - // act + // Act var querySet = queryParser.Parse(_queryCollectionMock.Object); - // assert + // Assert Assert.Empty(querySet.Filters); } @@ -139,11 +140,11 @@ public void Can_Disable_Sort() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.Sort)); + _requestMock + .Setup(m => m.DisabledQueryParams) + .Returns(QueryParams.Sort); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_requestMock.Object, new JsonApiOptions()); // act var querySet = queryParser.Parse(_queryCollectionMock.Object); @@ -164,11 +165,11 @@ public void Can_Disable_Include() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.Include)); + _requestMock + .Setup(m => m.DisabledQueryParams) + .Returns(QueryParams.Include); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_requestMock.Object, new JsonApiOptions()); // act var querySet = queryParser.Parse(_queryCollectionMock.Object); @@ -189,11 +190,11 @@ public void Can_Disable_Page() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.Page)); + _requestMock + .Setup(m => m.DisabledQueryParams) + .Returns(QueryParams.Page); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_requestMock.Object, new JsonApiOptions()); // act var querySet = queryParser.Parse(_queryCollectionMock.Object); @@ -214,16 +215,16 @@ public void Can_Disable_Fields() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.GetControllerAttribute()) - .Returns(new DisableQueryAttribute(QueryParams.Fields)); + _requestMock + .Setup(m => m.DisabledQueryParams) + .Returns(QueryParams.Fields); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_requestMock.Object, new JsonApiOptions()); // act var querySet = queryParser.Parse(_queryCollectionMock.Object); - // assert + // Assert Assert.Empty(querySet.Fields); } @@ -241,8 +242,8 @@ public void Can_Parse_Fields_Query() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.RequestEntity) + _requestMock + .Setup(m => m.GetContextEntity()) .Returns(new ContextEntity { EntityName = type, @@ -256,7 +257,7 @@ public void Can_Parse_Fields_Query() Relationships = new List() }); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_requestMock.Object, new JsonApiOptions()); // act var querySet = queryParser.Parse(_queryCollectionMock.Object); @@ -281,8 +282,8 @@ public void Throws_JsonApiException_If_Field_DoesNotExist() .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - _controllerContextMock - .Setup(m => m.RequestEntity) + _requestMock + .Setup(m => m.GetContextEntity()) .Returns(new ContextEntity { EntityName = type, @@ -290,7 +291,7 @@ public void Throws_JsonApiException_If_Field_DoesNotExist() Relationships = new List() }); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_requestMock.Object, new JsonApiOptions()); // act , assert var ex = Assert.Throws(() => queryParser.Parse(_queryCollectionMock.Object)); @@ -312,7 +313,7 @@ public void Can_Parse_Page_Size_Query(string value, int expectedValue, bool shou .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_requestMock.Object, new JsonApiOptions()); // act if (shouldThrow) @@ -342,7 +343,7 @@ public void Can_Parse_Page_Number_Query(string value, int expectedValue, bool sh .Setup(m => m.GetEnumerator()) .Returns(query.GetEnumerator()); - var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); + var queryParser = new QueryParser(_requestMock.Object, new JsonApiOptions()); // act if (shouldThrow)