From 8524da6e69b936d3dab32a98f9fe0ab258dc4736 Mon Sep 17 00:00:00 2001 From: Harro van der Kroft Date: Tue, 26 Nov 2019 13:19:24 +0100 Subject: [PATCH 1/8] docs: remove old index.md, add/restructure main readme.md --- README.md | 40 +++++++++++++++++++++------------- src/JsonApiDotNetCore/index.md | 4 ---- 2 files changed, 25 insertions(+), 19 deletions(-) delete mode 100644 src/JsonApiDotNetCore/index.md diff --git a/README.md b/README.md index 3c91c6492e..866c88f48a 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,25 @@

-# JSON API .Net Core +# JSON API .Net Core -[![Build status](https://ci.appveyor.com/api/projects/status/9fvgeoxdikwkom10?svg=true)](https://ci.appveyor.com/project/jaredcnance/jsonapidotnetcore) -[![Travis](https://travis-ci.org/json-api-dotnet/JsonApiDotNetCore.svg?branch=master)](https://travis-ci.org/json-api-dotnet/JsonApiDotNetCore) -[![NuGet](https://img.shields.io/nuget/v/JsonApiDotNetCore.svg)](https://www.nuget.org/packages/JsonApiDotNetCore/) -[![Join the chat at https://gitter.im/json-api-dotnet-core/Lobby](https://badges.gitter.im/json-api-dotnet-core/Lobby.svg)](https://gitter.im/json-api-dotnet-core/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![FIRST-TIMERS](https://img.shields.io/badge/first--timers--only-friendly-blue.svg)](http://www.firsttimersonly.com/) +[![Build status](https://ci.appveyor.com/api/projects/status/9fvgeoxdikwkom10?svg=true)](https://ci.appveyor.com/project/jaredcnance/jsonapidotnetcore) [![Travis](https://travis-ci.org/json-api-dotnet/JsonApiDotNetCore.svg?branch=master)](https://travis-ci.org/json-api-dotnet/JsonApiDotNetCore) [![NuGet](https://img.shields.io/nuget/v/JsonApiDotNetCore.svg)](https://www.nuget.org/packages/JsonApiDotNetCore/) [![Join the chat at https://gitter.im/json-api-dotnet-core/Lobby](https://badges.gitter.im/json-api-dotnet-core/Lobby.svg)](https://gitter.im/json-api-dotnet-core/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![FIRST-TIMERS](https://img.shields.io/badge/first--timers--only-friendly-blue.svg)](http://www.firsttimersonly.com/) A framework for building [json:api](http://jsonapi.org/) compliant web APIs. The ultimate goal of this library is to eliminate as much boilerplate as possible by offering out-of-the-box features such as sorting, filtering and pagination. You just need to focus on defining the resources and implementing your custom business logic. This library has been designed around dependency injection making extensibility incredibly easy. +## Table of Contents +- [Getting Started](#getting-started) +- [Related Projects](#related-projects) +- [Examples](#examples) +- [Compatibility](#compatibility) +- [Installation And Usage](#installation-and-usage) + - [Models](#models) + - [Controllers](#controllers) + - [Middleware](#middleware) +- [Development](#development) + - [Testing](#testing) + - [Cleaning](#cleaning) + ## Getting Started These are some steps you can take to help you understand what this project is and how you can use it: @@ -34,6 +43,15 @@ These are some steps you can take to help you understand what this project is an See the [examples](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/src/Examples) directory for up-to-date sample applications. There is also a [Todo List App](https://github.com/json-api-dotnet/TodoListExample) that includes a JADNC API and an EmberJs client. +## Compatibility + +A lot of changes were introduced in v4.0.0, the following chart should help you with compatibility issues between .NET Core versions + +| .NET Core Version | JADNC Version | +| ----------------- | ------------- | +| 2.* | v3.* | +| 3.* | v4.* | + ## Installation And Usage See [the documentation](https://json-api-dotnet.github.io/#/) for detailed usage. @@ -79,7 +97,7 @@ public class Startup } ``` -### Development +## Development Restore all NuGet packages with: @@ -109,13 +127,5 @@ Sometimes the compiled files can be dirty / corrupt from other branches / failed dotnet clean ``` -## Compatibility - -A lot of changes were introduced in v4.0.0, the following chart should help you with compatibility issues between .NET Core versions - -| .NET Core Version | JADNC Version | -| ----------------- | ------------- | -| 2.* | v3.* | -| 3.* | v4.* | diff --git a/src/JsonApiDotNetCore/index.md b/src/JsonApiDotNetCore/index.md deleted file mode 100644 index 3ae2506361..0000000000 --- a/src/JsonApiDotNetCore/index.md +++ /dev/null @@ -1,4 +0,0 @@ -# This is the **HOMEPAGE**. -Refer to [Markdown](http://daringfireball.net/projects/markdown/) for how to write markdown files. -## Quick Start Notes: -1. Add images to the *images* folder if the file is referencing an image. From 94c1c6abd41617a1eff3639eebffe6fc21f8ec58 Mon Sep 17 00:00:00 2001 From: Harro van der Kroft Date: Wed, 27 Nov 2019 14:45:26 +0100 Subject: [PATCH 2/8] feat: add test for 404 --- .../Spec/NonExistentResourceTests.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs new file mode 100644 index 0000000000..7e7f6da5ce --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Bogus; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCoreExample; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using Newtonsoft.Json; +using Xunit; +using Person = JsonApiDotNetCoreExample.Models.Person; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec +{ + [Collection("WebHostCollection")] + public class NonExistentResourceTests + { + private TestFixture _fixture; + private Faker _todoItemFaker; + private readonly Faker _personFaker; + + public NonExistentResourceTests(TestFixture fixture) + { + _fixture = fixture; + _todoItemFaker = new Faker() + .RuleFor(t => t.Description, f => f.Lorem.Sentence()) + .RuleFor(t => t.Ordinal, f => f.Random.Number()) + .RuleFor(t => t.CreatedDate, f => f.Date.Past()); + + _personFaker = new Faker() + .RuleFor(p => p.FirstName, f => f.Name.FirstName()) + .RuleFor(p => p.LastName, f => f.Name.LastName()); + } + + public class ErrorInnerMessage + { + [JsonProperty("title")] + public string Title; + [JsonProperty("status")] + public string Status; + } + public class ErrorMessage + { + [JsonProperty("errors")] + public List Errors; + } + [Fact] + public async Task Resource_UserNonExistent_ShouldReturn404WithCorrectError() + { + // Arrange + var context = _fixture.GetService(); + context.TodoItems.RemoveRange(context.TodoItems.ToList()); + var todoItem = _todoItemFaker.Generate(); + context.TodoItems.Add(todoItem); + await context.SaveChangesAsync(); + var nonExistentId = todoItem.Id; + context.TodoItems.Remove(todoItem); + context.SaveChanges(); + + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/todoItems/{nonExistentId}"; + var request = new HttpRequestMessage(httpMethod, route); + + // Act + var response = await _fixture.Client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + // Assert + var errorResult = JsonConvert.DeserializeObject(body); + var title = errorResult.Errors.First().Title; + Assert.Contains(title, "todoitem"); + Assert.Contains(title, "found"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + } +} From 00ac03045682e8151b9d5d78e7d26c7e3c8c0f23 Mon Sep 17 00:00:00 2001 From: Harro van der Kroft Date: Wed, 27 Nov 2019 14:52:24 +0100 Subject: [PATCH 3/8] chore: add hasMany and HasOne tests --- .../Spec/NonExistentResourceTests.cs | 66 +++++++++++++++++-- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs index 7e7f6da5ce..aa962e7f0c 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs @@ -49,6 +49,37 @@ public class ErrorMessage } [Fact] public async Task Resource_UserNonExistent_ShouldReturn404WithCorrectError() + { + // Arrange + var context = _fixture.GetService(); + context.People.RemoveRange(context.People.ToList()); + var person = _personFaker.Generate(); + context.People.Add(person); + await context.SaveChangesAsync(); + var nonExistentId = person.Id; + context.People.Remove(person); + context.SaveChanges(); + + var httpMethod = HttpMethod.Get; + var route = $"/api/v1/people/{nonExistentId}"; + var request = new HttpRequestMessage(httpMethod, route); + + // Act + var response = await _fixture.Client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + // Assert + var errorResult = JsonConvert.DeserializeObject(body); + var errorParsed = errorResult.Errors.First(); + var title = errorParsed.Title; + var code = errorParsed.Status; + Assert.Contains(title, "person"); + Assert.Contains(title, "found"); + Assert.Equal("404", code); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + [Fact] + public async Task ResourceRelatedHasOne_TodoItemExistentOwnerIsNonExistent_ShouldReturn200WithNullData() { // Arrange var context = _fixture.GetService(); @@ -56,12 +87,37 @@ public async Task Resource_UserNonExistent_ShouldReturn404WithCorrectError() var todoItem = _todoItemFaker.Generate(); context.TodoItems.Add(todoItem); await context.SaveChangesAsync(); - var nonExistentId = todoItem.Id; - context.TodoItems.Remove(todoItem); - context.SaveChanges(); + var existingId = todoItem.Id; + + var httpMethod = HttpMethod.Get; + var route = $"/api/v1/todoItems/{existingId}/people"; + var request = new HttpRequestMessage(httpMethod, route); + + // Act + var response = await _fixture.Client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + // Assert + var errorResult = JsonConvert.DeserializeObject(body); + var title = errorResult.Errors.First().Title; + Assert.Contains(title, "todoitem"); + Assert.Contains(title, "found"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task ResourceRelatedHasMany_PersonExistsToDoItemDoesNot_ShouldReturn200WithNullData() + { + // Arrange + var context = _fixture.GetService(); + context.TodoItems.RemoveRange(context.TodoItems.ToList()); + var todoItem = _todoItemFaker.Generate(); + context.TodoItems.Add(todoItem); + await context.SaveChangesAsync(); + var existingId = todoItem.Id; - var httpMethod = new HttpMethod("GET"); - var route = $"/api/v1/todoItems/{nonExistentId}"; + var httpMethod = HttpMethod.Get; + var route = $"/api/v1/todoItems/{existingId}/people"; var request = new HttpRequestMessage(httpMethod, route); // Act From b499d9c31ef5d7736809683dd276cfed8f251fe0 Mon Sep 17 00:00:00 2001 From: Harro van der Kroft Date: Wed, 27 Nov 2019 16:07:00 +0100 Subject: [PATCH 4/8] feat: add 404 with correct type for non-found single resource --- .../Controllers/BaseJsonApiController.cs | 19 +++--- .../Formatters/JsonApiWriter.cs | 58 +++++++++++++------ .../Exceptions/ResourceNotFoundException.cs | 13 +++++ .../Middleware/DefaultExceptionFilter.cs | 10 ++-- .../Spec/NonExistentResourceTests.cs | 13 ++--- 5 files changed, 77 insertions(+), 36 deletions(-) create mode 100644 src/JsonApiDotNetCore/Internal/Exceptions/ResourceNotFoundException.cs diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index c241c5b81d..09724375b8 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -101,18 +101,25 @@ public virtual async Task GetAsync() public virtual async Task GetAsync(TId id) { - if (_getById == null) throw Exceptions.UnSupportedRequestMethod; + if (_getById == null) { + throw Exceptions.UnSupportedRequestMethod; + } var entity = await _getById.GetAsync(id); if (entity == null) { - // remove the null argument as soon as this has been resolved: - // https://github.com/aspnet/AspNetCore/issues/16969 - return NotFound(null); + return NoResultFound(); } return Ok(entity); } + private NotFoundObjectResult NoResultFound() + { + // remove the null argument as soon as this has been resolved: + // https://github.com/aspnet/AspNetCore/issues/16969 + return NotFound(null); + } + public virtual async Task GetRelationshipsAsync(TId id, string relationshipName) { if (_getRelationships == null) @@ -120,9 +127,7 @@ public virtual async Task GetRelationshipsAsync(TId id, string re var relationship = await _getRelationships.GetRelationshipsAsync(id, relationshipName); if (relationship == null) { - // remove the null argument as soon as this has been resolved: - // https://github.com/aspnet/AspNetCore/issues/16969 - return NotFound(null); + return NoResultFound(); } return Ok(relationship); diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs b/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs index aa8b779992..dbfc22f0d3 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs @@ -2,6 +2,8 @@ using System.Text; using System.Threading.Tasks; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Serialization.Server; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Formatters; @@ -18,11 +20,14 @@ namespace JsonApiDotNetCore.Formatters public class JsonApiWriter : IJsonApiWriter { private readonly ILogger _logger; + private readonly ICurrentRequest _currentRequest; private readonly IJsonApiSerializer _serializer; public JsonApiWriter(IJsonApiSerializer serializer, - ILoggerFactory loggerFactory) + ILoggerFactory loggerFactory, + ICurrentRequest currentRequest) { + _currentRequest = currentRequest; _serializer = serializer; _logger = loggerFactory.CreateLogger(); } @@ -30,40 +35,57 @@ public JsonApiWriter(IJsonApiSerializer serializer, public async Task WriteAsync(OutputFormatterWriteContext context) { if (context == null) + { throw new ArgumentNullException(nameof(context)); + } var response = context.HttpContext.Response; using var writer = context.WriterFactory(response.Body, Encoding.UTF8); string responseContent; - if (_serializer == null) + if (response.StatusCode == 404) { - responseContent = JsonConvert.SerializeObject(context.Object); + var requestedModel = _currentRequest.GetRequestResource(); + var errors = new ErrorCollection(); + errors.Add(new Error(404, $"The resource with type '{requestedModel.ResourceName}' and id 'unknown' could not be found")); + responseContent = _serializer.Serialize(errors); + response.StatusCode = 404; } else { - response.ContentType = Constants.ContentType; - try + + if (_serializer == null) { - if (context.Object is ProblemDetails pd) + responseContent = JsonConvert.SerializeObject(context.Object); + } + else + { + response.ContentType = Constants.ContentType; + try + { + if (context.Object is ProblemDetails pd) + { + var errors = new ErrorCollection(); + errors.Add(new Error(pd.Status.Value, pd.Title, pd.Detail)); + responseContent = _serializer.Serialize(errors); + + } + else + { + responseContent = _serializer.Serialize(context.Object); + } + } + catch (Exception e) { + _logger?.LogError(new EventId(), e, "An error ocurred while formatting the response"); var errors = new ErrorCollection(); - errors.Add(new Error(pd.Status.Value, pd.Title, pd.Detail)); + errors.Add(new Error(500, e.Message, ErrorMeta.FromException(e))); responseContent = _serializer.Serialize(errors); - } else - { - responseContent = _serializer.Serialize(context.Object); + response.StatusCode = 500; } } - catch (Exception e) - { - _logger?.LogError(new EventId(), e, "An error ocurred while formatting the response"); - var errors = new ErrorCollection(); - errors.Add(new Error(500, e.Message, ErrorMeta.FromException(e))); - responseContent = _serializer.Serialize(errors); - response.StatusCode = 500; - } } + await writer.WriteAsync(responseContent); await writer.FlushAsync(); } diff --git a/src/JsonApiDotNetCore/Internal/Exceptions/ResourceNotFoundException.cs b/src/JsonApiDotNetCore/Internal/Exceptions/ResourceNotFoundException.cs new file mode 100644 index 0000000000..6f72cfc9b5 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Exceptions/ResourceNotFoundException.cs @@ -0,0 +1,13 @@ +using System; + +namespace JsonApiDotNetCore.Internal +{ + public class ResourceNotFoundException : Exception + { + private readonly ErrorCollection _errors = new ErrorCollection(); + + public ResourceNotFoundException() + { } + + } +} diff --git a/src/JsonApiDotNetCore/Middleware/DefaultExceptionFilter.cs b/src/JsonApiDotNetCore/Middleware/DefaultExceptionFilter.cs index b6c82b27e3..3ed2da84a5 100644 --- a/src/JsonApiDotNetCore/Middleware/DefaultExceptionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/DefaultExceptionFilter.cs @@ -1,7 +1,9 @@ -using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Managers.Contracts; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Logging; +using System.Collections.Generic; namespace JsonApiDotNetCore.Middleware { @@ -10,19 +12,19 @@ namespace JsonApiDotNetCore.Middleware /// public class DefaultExceptionFilter : ActionFilterAttribute, IExceptionFilter { + private readonly ICurrentRequest _currentRequest; private readonly ILogger _logger; - public DefaultExceptionFilter(ILoggerFactory loggerFactory) + public DefaultExceptionFilter(ILoggerFactory loggerFactory, ICurrentRequest currentRequest) { + _currentRequest = currentRequest; _logger = loggerFactory.CreateLogger(); } public void OnException(ExceptionContext context) { _logger?.LogError(new EventId(), context.Exception, "An unhandled exception occurred during the request"); - var jsonApiException = JsonApiExceptionFactory.GetException(context.Exception); - var error = jsonApiException.GetError(); var result = new ObjectResult(error) { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs index aa962e7f0c..a9ab8be4f9 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs @@ -48,20 +48,19 @@ public class ErrorMessage public List Errors; } [Fact] - public async Task Resource_UserNonExistent_ShouldReturn404WithCorrectError() + public async Task Resource_PersonNonExistent_ShouldReturn404WithCorrectError() { // Arrange var context = _fixture.GetService(); - context.People.RemoveRange(context.People.ToList()); var person = _personFaker.Generate(); context.People.Add(person); await context.SaveChangesAsync(); - var nonExistentId = person.Id; + var nonExistingId = person.Id; context.People.Remove(person); context.SaveChanges(); var httpMethod = HttpMethod.Get; - var route = $"/api/v1/people/{nonExistentId}"; + var route = $"/api/v1/people/{nonExistingId}"; var request = new HttpRequestMessage(httpMethod, route); // Act @@ -73,8 +72,9 @@ public async Task Resource_UserNonExistent_ShouldReturn404WithCorrectError() var errorParsed = errorResult.Errors.First(); var title = errorParsed.Title; var code = errorParsed.Status; - Assert.Contains(title, "person"); - Assert.Contains(title, "found"); + Assert.Contains("found", title); + Assert.Contains("people", title); + Assert.Contains(nonExistingId.ToString(), title); Assert.Equal("404", code); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -100,7 +100,6 @@ public async Task ResourceRelatedHasOne_TodoItemExistentOwnerIsNonExistent_Shoul // Assert var errorResult = JsonConvert.DeserializeObject(body); var title = errorResult.Errors.First().Title; - Assert.Contains(title, "todoitem"); Assert.Contains(title, "found"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } From d12d1c2314569fe7419178a9aae992241e5ba752 Mon Sep 17 00:00:00 2001 From: Harro van der Kroft Date: Mon, 23 Dec 2019 16:46:20 +0100 Subject: [PATCH 5/8] feat: working tests, fixed ID not being present in error --- .../Controllers/BaseJsonApiController.cs | 45 +++++++++++----- .../Formatters/JsonApiWriter.cs | 4 +- .../Acceptance/Spec/EndToEndTest.cs | 4 +- .../Spec/NonExistentResourceTests.cs | 53 +++++++++---------- 4 files changed, 60 insertions(+), 46 deletions(-) diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index 09724375b8..7453f81f19 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Extensions; @@ -23,7 +25,7 @@ public class BaseJsonApiController private readonly IDeleteService _delete; private readonly ILogger> _logger; private readonly IJsonApiOptions _jsonApiOptions; - + public BaseJsonApiController( IJsonApiOptions jsonApiOptions, IResourceService resourceService, @@ -101,7 +103,8 @@ public virtual async Task GetAsync() public virtual async Task GetAsync(TId id) { - if (_getById == null) { + if (_getById == null) + { throw Exceptions.UnSupportedRequestMethod; } var entity = await _getById.GetAsync(id); @@ -122,21 +125,37 @@ private NotFoundObjectResult NoResultFound() public virtual async Task GetRelationshipsAsync(TId id, string relationshipName) { - if (_getRelationships == null) - throw Exceptions.UnSupportedRequestMethod; - var relationship = await _getRelationships.GetRelationshipsAsync(id, relationshipName); - if (relationship == null) - { - return NoResultFound(); - } - - return Ok(relationship); + return await GetRelationshipInternal(id, relationshipName, relationshipInUrl: true); } public virtual async Task GetRelationshipAsync(TId id, string relationshipName) { - if (_getRelationship == null) throw Exceptions.UnSupportedRequestMethod; - var relationship = await _getRelationship.GetRelationshipAsync(id, relationshipName); + return await GetRelationshipInternal(id, relationshipName, relationshipInUrl: false); + } + + protected virtual async Task GetRelationshipInternal(TId id, string relationshipName, bool relationshipInUrl) + { + if (_getRelationship == null) + { + throw Exceptions.UnSupportedRequestMethod; + } + object relationship; + if (relationshipInUrl) + { + relationship = await _getRelationship.GetRelationshipAsync(id, relationshipName); + } + else + { + relationship = await _getRelationship.GetRelationshipAsync(id, relationshipName); + } + if(relationship == null) + { + return Ok(null); + } + if (((IEnumerable)relationship).Count() == 0) + { + return Ok(null); + } return Ok(relationship); } diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs b/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs index dbfc22f0d3..19595494ec 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs @@ -47,13 +47,12 @@ public async Task WriteAsync(OutputFormatterWriteContext context) { var requestedModel = _currentRequest.GetRequestResource(); var errors = new ErrorCollection(); - errors.Add(new Error(404, $"The resource with type '{requestedModel.ResourceName}' and id 'unknown' could not be found")); + errors.Add(new Error(404, $"The resource with type '{requestedModel.ResourceName}' and id '{_currentRequest.BaseId}' could not be found")); responseContent = _serializer.Serialize(errors); response.StatusCode = 404; } else { - if (_serializer == null) { responseContent = JsonConvert.SerializeObject(context.Object); @@ -68,7 +67,6 @@ public async Task WriteAsync(OutputFormatterWriteContext context) var errors = new ErrorCollection(); errors.Add(new Error(pd.Status.Value, pd.Title, pd.Detail)); responseContent = _serializer.Serialize(errors); - } else { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs index e3cb86dd9d..1cbd66bf80 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs @@ -36,7 +36,7 @@ public FunctionalTestCollection(TFactory factory) ClearDbContext(); } - protected Task<(string, HttpResponseMessage)> Get(string route) + protected Task<(string Body, HttpResponseMessage Response)> Get(string route) { return SendRequest("GET", route); } @@ -109,7 +109,7 @@ protected void ClearDbContext() _dbContext.SaveChanges(); } - private async Task<(string, HttpResponseMessage)> SendRequest(string method, string route, string content = null) + private async Task<(string body, HttpResponseMessage response)> SendRequest(string method, string route, string content = null) { var request = new HttpRequestMessage(new HttpMethod(method), route); if (content != null) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs index a9ab8be4f9..03de5b6d23 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs @@ -5,7 +5,9 @@ using System.Net.Http; using System.Threading.Tasks; using Bogus; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization.Client; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -15,16 +17,15 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec { - [Collection("WebHostCollection")] - public class NonExistentResourceTests + public class NonExistentResourceTests : FunctionalTestCollection { - private TestFixture _fixture; + private StandardApplicationFactory _factory; private Faker _todoItemFaker; private readonly Faker _personFaker; - public NonExistentResourceTests(TestFixture fixture) + public NonExistentResourceTests(StandardApplicationFactory factory) : base(factory) { - _fixture = fixture; + _factory = factory; _todoItemFaker = new Faker() .RuleFor(t => t.Description, f => f.Lorem.Sentence()) .RuleFor(t => t.Ordinal, f => f.Random.Number()) @@ -51,7 +52,7 @@ public class ErrorMessage public async Task Resource_PersonNonExistent_ShouldReturn404WithCorrectError() { // Arrange - var context = _fixture.GetService(); + var context = _factory.GetService(); var person = _personFaker.Generate(); context.People.Add(person); await context.SaveChangesAsync(); @@ -59,12 +60,10 @@ public async Task Resource_PersonNonExistent_ShouldReturn404WithCorrectError() context.People.Remove(person); context.SaveChanges(); - var httpMethod = HttpMethod.Get; var route = $"/api/v1/people/{nonExistingId}"; - var request = new HttpRequestMessage(httpMethod, route); // Act - var response = await _fixture.Client.SendAsync(request); + var response = (await Get(route)).Response; var body = await response.Content.ReadAsStringAsync(); // Assert @@ -79,36 +78,34 @@ public async Task Resource_PersonNonExistent_ShouldReturn404WithCorrectError() Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] - public async Task ResourceRelatedHasOne_TodoItemExistentOwnerIsNonExistent_ShouldReturn200WithNullData() + public async Task ResourceRelatedHasOne_TodoItemExistentToOneRelationshipIsNonExistent_ShouldReturn200WithNullData() { // Arrange - var context = _fixture.GetService(); + var context = _factory.GetService(); context.TodoItems.RemoveRange(context.TodoItems.ToList()); var todoItem = _todoItemFaker.Generate(); context.TodoItems.Add(todoItem); await context.SaveChangesAsync(); var existingId = todoItem.Id; + var deserializer = new ResponseDeserializer(_factory.GetService()); - var httpMethod = HttpMethod.Get; - var route = $"/api/v1/todoItems/{existingId}/people"; - var request = new HttpRequestMessage(httpMethod, route); + var route = $"/api/v1/todoItems/{existingId}/oneToOnePerson"; // Act - var response = await _fixture.Client.SendAsync(request); + var response = (await Get(route)).Response; var body = await response.Content.ReadAsStringAsync(); // Assert - var errorResult = JsonConvert.DeserializeObject(body); - var title = errorResult.Errors.First().Title; - Assert.Contains(title, "found"); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + var document = deserializer.DeserializeList(body); + Assert.Null(document.Data); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); } [Fact] - public async Task ResourceRelatedHasMany_PersonExistsToDoItemDoesNot_ShouldReturn200WithNullData() + public async Task ResourceRelatedHasMany_TodoItemExistsToManyRelationshipHasNoData_ShouldReturn200WithNullData() { // Arrange - var context = _fixture.GetService(); + var context = _factory.GetService(); context.TodoItems.RemoveRange(context.TodoItems.ToList()); var todoItem = _todoItemFaker.Generate(); context.TodoItems.Add(todoItem); @@ -116,19 +113,19 @@ public async Task ResourceRelatedHasMany_PersonExistsToDoItemDoesNot_ShouldRetur var existingId = todoItem.Id; var httpMethod = HttpMethod.Get; - var route = $"/api/v1/todoItems/{existingId}/people"; + var route = $"/api/v1/todoItems/{existingId}/stakeHolders"; var request = new HttpRequestMessage(httpMethod, route); + var deserializer = new ResponseDeserializer(_factory.GetService()); + // Act - var response = await _fixture.Client.SendAsync(request); + var response = await _factory.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); // Assert - var errorResult = JsonConvert.DeserializeObject(body); - var title = errorResult.Errors.First().Title; - Assert.Contains(title, "todoitem"); - Assert.Contains(title, "found"); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + var parsed = deserializer.DeserializeList(body); + Assert.Null(parsed.Data); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); } } } From 5b40e7ea33eb2a2a1b1482fad83e94615e7190a5 Mon Sep 17 00:00:00 2001 From: Harro van der Kroft Date: Tue, 24 Dec 2019 11:07:22 +0100 Subject: [PATCH 6/8] chore: fix for tests --- .../Controllers/BaseJsonApiController.cs | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index 7453f81f19..d31681a7c6 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -135,26 +135,35 @@ public virtual async Task GetRelationshipAsync(TId id, string rel protected virtual async Task GetRelationshipInternal(TId id, string relationshipName, bool relationshipInUrl) { - if (_getRelationship == null) - { - throw Exceptions.UnSupportedRequestMethod; - } + object relationship; if (relationshipInUrl) { - relationship = await _getRelationship.GetRelationshipAsync(id, relationshipName); + if (_getRelationships == null) + { + throw Exceptions.UnSupportedRequestMethod; + } + relationship = await _getRelationships.GetRelationshipsAsync(id, relationshipName); } else { + if (_getRelationship == null) + { + throw Exceptions.UnSupportedRequestMethod; + } relationship = await _getRelationship.GetRelationshipAsync(id, relationshipName); } - if(relationship == null) + if (relationship == null) { return Ok(null); } - if (((IEnumerable)relationship).Count() == 0) + + if (relationship.GetType() != typeof(T)) { - return Ok(null); + if (((IEnumerable)relationship).Count() == 0) + { + return Ok(null); + } } return Ok(relationship); } From 63c9e6e1e5ec0f58fa5f7f81b9ae8e7c50d5421d Mon Sep 17 00:00:00 2001 From: Harro van der Kroft Date: Tue, 24 Dec 2019 15:03:24 +0100 Subject: [PATCH 7/8] chore: idk --- .../Services/DefaultResourceService.cs | 26 ++++++++++++------- .../Spec/NonExistentResourceTests.cs | 26 ++++++++++++------- .../IServiceCollectionExtensionsTests.cs | 4 +-- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/JsonApiDotNetCore/Services/DefaultResourceService.cs b/src/JsonApiDotNetCore/Services/DefaultResourceService.cs index cb4328947a..629dbefd02 100644 --- a/src/JsonApiDotNetCore/Services/DefaultResourceService.cs +++ b/src/JsonApiDotNetCore/Services/DefaultResourceService.cs @@ -125,7 +125,7 @@ public virtual async Task GetAsync(TId id) } // triggered by GET /articles/1/relationships/{relationshipName} - public virtual async Task GetRelationshipsAsync(TId id, string relationshipName) + public virtual async Task<(TResource model, bool emptyResults)> GetRelationshipsAsync(TId id, string relationshipName) { var relationship = GetRelationship(relationshipName); @@ -135,14 +135,22 @@ public virtual async Task GetRelationshipsAsync(TId id, string relati // TODO: it would be better if we could distinguish whether or not the relationship was not found, // vs the relationship not being set on the instance of T - var entityQuery = ApplyInclude(_repository.Get(id), chainPrefix: new List { relationship }); + var baseQuery = _repository.Get(id); + var entityQuery = ApplyInclude(baseQuery, chainPrefix: new List { relationship }); + var entity = await _repository.FirstOrDefaultAsync(entityQuery); - if (entity == null) - { - /// TODO: this does not make sense. If the **parent** entity is not found, this error is thrown? - /// this error should be thrown when the relationship is not found. - throw new JsonApiException(404, $"Relationship '{relationshipName}' not found."); - } + + + // lol + var relationshipValue = typeof(TResource).GetProperty(relationship.EntityPropertyName).GetValue(entity) ; + var relEmpty = relationshipValue == null; + + //if (entity == null) + //{ + // /// TODO: this does not make sense. If the **parent** entity is not found, this error is thrown? + // /// this error should be thrown when the relationship is not found. + // throw new JsonApiException(404, $"Relationship '{relationshipName}' not found."); + //} if (!IsNull(_hookExecutor, entity)) { // AfterRead and OnReturn resource hook execution. @@ -150,7 +158,7 @@ public virtual async Task GetRelationshipsAsync(TId id, string relati entity = _hookExecutor.OnReturn(AsList(entity), ResourcePipeline.GetRelationship).SingleOrDefault(); } - return entity; + return (entity, relEmpty); } // triggered by GET /articles/1/{relationshipName} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs index 03de5b6d23..cab735bfcd 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/NonExistentResourceTests.cs @@ -48,6 +48,7 @@ public class ErrorMessage [JsonProperty("errors")] public List Errors; } + [Fact] public async Task Resource_PersonNonExistent_ShouldReturn404WithCorrectError() { @@ -59,7 +60,6 @@ public async Task Resource_PersonNonExistent_ShouldReturn404WithCorrectError() var nonExistingId = person.Id; context.People.Remove(person); context.SaveChanges(); - var route = $"/api/v1/people/{nonExistingId}"; // Act @@ -77,8 +77,11 @@ public async Task Resource_PersonNonExistent_ShouldReturn404WithCorrectError() Assert.Equal("404", code); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } - [Fact] - public async Task ResourceRelatedHasOne_TodoItemExistentToOneRelationshipIsNonExistent_ShouldReturn200WithNullData() + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ResourceRelatedHasOne_TodoItemExistentToOneRelationshipIsNonExistent_ShouldReturn200WithNullData(bool full) { // Arrange var context = _factory.GetService(); @@ -89,7 +92,9 @@ public async Task ResourceRelatedHasOne_TodoItemExistentToOneRelationshipIsNonEx var existingId = todoItem.Id; var deserializer = new ResponseDeserializer(_factory.GetService()); - var route = $"/api/v1/todoItems/{existingId}/oneToOnePerson"; + var appendix = full ? "oneToOnePerson" : "relationships/oneToOnePerson"; + var route = $"/api/v1/todoItems/{existingId}/{appendix}"; + // Act var response = (await Get(route)).Response; @@ -101,8 +106,10 @@ public async Task ResourceRelatedHasOne_TodoItemExistentToOneRelationshipIsNonEx Assert.Equal(HttpStatusCode.OK, response.StatusCode); } - [Fact] - public async Task ResourceRelatedHasMany_TodoItemExistsToManyRelationshipHasNoData_ShouldReturn200WithNullData() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ResourceRelatedHasMany_TodoItemExistsToManyRelationshipHasNoData_ShouldReturn200WithNullData(bool withBaseEntity) { // Arrange var context = _factory.GetService(); @@ -113,9 +120,9 @@ public async Task ResourceRelatedHasMany_TodoItemExistsToManyRelationshipHasNoDa var existingId = todoItem.Id; var httpMethod = HttpMethod.Get; - var route = $"/api/v1/todoItems/{existingId}/stakeHolders"; + var appendix = withBaseEntity ? "stakeHolders" : "relationships/stakeHolders"; + var route = $"/api/v1/todoItems/{existingId}/{appendix}"; var request = new HttpRequestMessage(httpMethod, route); - var deserializer = new ResponseDeserializer(_factory.GetService()); // Act @@ -124,7 +131,8 @@ public async Task ResourceRelatedHasMany_TodoItemExistsToManyRelationshipHasNoDa // Assert var parsed = deserializer.DeserializeList(body); - Assert.Null(parsed.Data); + Assert.NotNull(parsed.Data); + Assert.Empty(parsed.Data); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } } diff --git a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index 7cae18b8df..5b75ab28b5 100644 --- a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -165,7 +165,7 @@ private class IntResourceService : IResourceService public Task> GetAsync() => throw new NotImplementedException(); public Task GetAsync(int id) => throw new NotImplementedException(); public Task GetRelationshipAsync(int id, string relationshipName) => throw new NotImplementedException(); - public Task GetRelationshipsAsync(int id, string relationshipName) => throw new NotImplementedException(); + public Task<(IntResource model, bool emptyResults)> GetRelationshipsAsync(int id, string relationshipName) => throw new NotImplementedException(); public Task UpdateAsync(int id, IntResource entity) => throw new NotImplementedException(); public Task UpdateRelationshipsAsync(int id, string relationshipName, object relationships) => throw new NotImplementedException(); } @@ -177,7 +177,7 @@ private class GuidResourceService : IResourceService public Task> GetAsync() => throw new NotImplementedException(); public Task GetAsync(Guid id) => throw new NotImplementedException(); public Task GetRelationshipAsync(Guid id, string relationshipName) => throw new NotImplementedException(); - public Task GetRelationshipsAsync(Guid id, string relationshipName) => throw new NotImplementedException(); + public Task<(GuidResource model, emptyResults bool)> GetRelationshipsAsync(Guid id, string relationshipName) => throw new NotImplementedException(); public Task UpdateAsync(Guid id, GuidResource entity) => throw new NotImplementedException(); public Task UpdateRelationshipsAsync(Guid id, string relationshipName, object relationships) => throw new NotImplementedException(); } From 3d051d750b21af6a34d2f4f1c9b6f19b943bc6e9 Mon Sep 17 00:00:00 2001 From: Harro van der Kroft Date: Tue, 24 Dec 2019 16:01:33 +0100 Subject: [PATCH 8/8] chore:sdlfkj --- .../Controllers/BaseJsonApiController.cs | 2 +- .../Server/Builders/LinkBuilder.cs | 4 ++++ .../Services/DefaultResourceService.cs | 23 ++++++++++++++++--- .../IServiceCollectionExtensionsTests.cs | 4 ++-- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index d31681a7c6..f44927ccef 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -155,7 +155,7 @@ protected virtual async Task GetRelationshipInternal(TId id, stri } if (relationship == null) { - return Ok(null); + return Ok(relationship); } if (relationship.GetType() != typeof(T)) diff --git a/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs index 8fe73a289d..0ae8894eaf 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs @@ -110,6 +110,10 @@ public ResourceLinks GetResourceLinks(string resourceName, string id) /// public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable parent) { + if(parent == null) + { + return null; + } var parentResourceContext = _provider.GetResourceContext(parent.GetType()); var childNavigation = relationship.PublicRelationshipName; RelationshipLinks links = null; diff --git a/src/JsonApiDotNetCore/Services/DefaultResourceService.cs b/src/JsonApiDotNetCore/Services/DefaultResourceService.cs index 629dbefd02..73ead184f5 100644 --- a/src/JsonApiDotNetCore/Services/DefaultResourceService.cs +++ b/src/JsonApiDotNetCore/Services/DefaultResourceService.cs @@ -11,6 +11,7 @@ using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Query; using JsonApiDotNetCore.Extensions; +using System.Collections; namespace JsonApiDotNetCore.Services { @@ -124,8 +125,9 @@ public virtual async Task GetAsync(TId id) return entity; } + // triggered by GET /articles/1/relationships/{relationshipName} - public virtual async Task<(TResource model, bool emptyResults)> GetRelationshipsAsync(TId id, string relationshipName) + public virtual async Task GetRelationshipsAsync(TId id, string relationshipName) { var relationship = GetRelationship(relationshipName); @@ -142,9 +144,22 @@ public virtual async Task GetAsync(TId id) // lol - var relationshipValue = typeof(TResource).GetProperty(relationship.EntityPropertyName).GetValue(entity) ; + var relationshipValue = typeof(TResource).GetProperty(relationship.InternalRelationshipName).GetValue(entity) ; var relEmpty = relationshipValue == null; + if(relationshipValue == null) + { + return null; + } + var listCast = (IList) relationshipValue; + if(listCast != null) + { + if(listCast.Count == 0) + { + return null; + } + } + //if (entity == null) //{ // /// TODO: this does not make sense. If the **parent** entity is not found, this error is thrown? @@ -158,7 +173,7 @@ public virtual async Task GetAsync(TId id) entity = _hookExecutor.OnReturn(AsList(entity), ResourcePipeline.GetRelationship).SingleOrDefault(); } - return (entity, relEmpty); + return entity; } // triggered by GET /articles/1/{relationshipName} @@ -347,7 +362,9 @@ private RelationshipAttribute GetRelationship(string relationshipName) { var relationship = _currentRequestResource.Relationships.Single(r => r.Is(relationshipName)); if (relationship == null) + { throw new JsonApiException(422, $"Relationship '{relationshipName}' does not exist on resource '{typeof(TResource)}'."); + } return relationship; } diff --git a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index 5b75ab28b5..7cae18b8df 100644 --- a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -165,7 +165,7 @@ private class IntResourceService : IResourceService public Task> GetAsync() => throw new NotImplementedException(); public Task GetAsync(int id) => throw new NotImplementedException(); public Task GetRelationshipAsync(int id, string relationshipName) => throw new NotImplementedException(); - public Task<(IntResource model, bool emptyResults)> GetRelationshipsAsync(int id, string relationshipName) => throw new NotImplementedException(); + public Task GetRelationshipsAsync(int id, string relationshipName) => throw new NotImplementedException(); public Task UpdateAsync(int id, IntResource entity) => throw new NotImplementedException(); public Task UpdateRelationshipsAsync(int id, string relationshipName, object relationships) => throw new NotImplementedException(); } @@ -177,7 +177,7 @@ private class GuidResourceService : IResourceService public Task> GetAsync() => throw new NotImplementedException(); public Task GetAsync(Guid id) => throw new NotImplementedException(); public Task GetRelationshipAsync(Guid id, string relationshipName) => throw new NotImplementedException(); - public Task<(GuidResource model, emptyResults bool)> GetRelationshipsAsync(Guid id, string relationshipName) => throw new NotImplementedException(); + public Task GetRelationshipsAsync(Guid id, string relationshipName) => throw new NotImplementedException(); public Task UpdateAsync(Guid id, GuidResource entity) => throw new NotImplementedException(); public Task UpdateRelationshipsAsync(Guid id, string relationshipName, object relationships) => throw new NotImplementedException(); }