Skip to content

Return 404 or 200 on error/non existent resource #636

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from
Closed
40 changes: 25 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,25 @@
<img src ="https://raw.githubusercontent.com/json-api-dotnet/JsonApiDotnetCore/master/logo.png" />
</p>

# JSON API .Net Core
# JSON API .Net Core <!-- omit in toc -->

[![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 <!-- omit in toc -->
- [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:
Expand All @@ -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.
Expand Down Expand Up @@ -79,7 +97,7 @@ public class Startup
}
```

### Development
## Development

Restore all NuGet packages with:

Expand Down Expand Up @@ -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.* |


69 changes: 51 additions & 18 deletions src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Extensions;
Expand All @@ -23,7 +25,7 @@ public class BaseJsonApiController<T, TId>
private readonly IDeleteService<T, TId> _delete;
private readonly ILogger<BaseJsonApiController<T, TId>> _logger;
private readonly IJsonApiOptions _jsonApiOptions;

public BaseJsonApiController(
IJsonApiOptions jsonApiOptions,
IResourceService<T, TId> resourceService,
Expand Down Expand Up @@ -101,37 +103,68 @@ public virtual async Task<IActionResult> GetAsync()

public virtual async Task<IActionResult> 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);
}

public virtual async Task<IActionResult> GetRelationshipsAsync(TId id, string relationshipName)
private NotFoundObjectResult NoResultFound()
{
if (_getRelationships == null)
throw Exceptions.UnSupportedRequestMethod;
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);
}
// remove the null argument as soon as this has been resolved:
// https://github.com/aspnet/AspNetCore/issues/16969
return NotFound(null);
}

return Ok(relationship);
public virtual async Task<IActionResult> GetRelationshipsAsync(TId id, string relationshipName)
{
return await GetRelationshipInternal(id, relationshipName, relationshipInUrl: true);
}

public virtual async Task<IActionResult> 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<IActionResult> GetRelationshipInternal(TId id, string relationshipName, bool relationshipInUrl)
{

object relationship;
if (relationshipInUrl)
{
if (_getRelationships == null)
{
throw Exceptions.UnSupportedRequestMethod;
}
relationship = await _getRelationships.GetRelationshipsAsync(id, relationshipName);
}
else
{
if (_getRelationship == null)
Copy link
Contributor

@bart-degreed bart-degreed Jan 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is duplicated in both true and false blocks of the outer if statement. I believe it can be moved to the top, outside the if clauses, so it appears only once.
They are actually different: Relationship vs Relationships

{
throw Exceptions.UnSupportedRequestMethod;
}
relationship = await _getRelationship.GetRelationshipAsync(id, relationshipName);
}
if (relationship == null)
{
return Ok(relationship);
}

if (relationship.GetType() != typeof(T))
{
if (((IEnumerable<object>)relationship).Count() == 0)
{
return Ok(null);
}
}
return Ok(relationship);
}

Expand Down
56 changes: 38 additions & 18 deletions src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,52 +20,70 @@ namespace JsonApiDotNetCore.Formatters
public class JsonApiWriter : IJsonApiWriter
{
private readonly ILogger<JsonApiWriter> _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<JsonApiWriter>();
}

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 '{_currentRequest.BaseId}' could not be found"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resource of type...
and should "id" be uppercase?

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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;

namespace JsonApiDotNetCore.Internal
{
public class ResourceNotFoundException : Exception
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type appears to be unused and can be removed.

{
private readonly ErrorCollection _errors = new ErrorCollection();

public ResourceNotFoundException()
{ }

}
}
10 changes: 6 additions & 4 deletions src/JsonApiDotNetCore/Middleware/DefaultExceptionFilter.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -10,19 +12,19 @@ namespace JsonApiDotNetCore.Middleware
/// </summary>
public class DefaultExceptionFilter : ActionFilterAttribute, IExceptionFilter
{
private readonly ICurrentRequest _currentRequest;
private readonly ILogger _logger;

public DefaultExceptionFilter(ILoggerFactory loggerFactory)
public DefaultExceptionFilter(ILoggerFactory loggerFactory, ICurrentRequest currentRequest)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The private field that is assigned from this parameter is never used.

{
_currentRequest = currentRequest;
_logger = loggerFactory.CreateLogger<DefaultExceptionFilter>();
}

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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ public ResourceLinks GetResourceLinks(string resourceName, string id)
/// <inheritdoc/>
public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable parent)
{
if(parent == null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there should be a space between the if keyword and the opening parenthesis.

{
return null;
}
var parentResourceContext = _provider.GetResourceContext(parent.GetType());
var childNavigation = relationship.PublicRelationshipName;
RelationshipLinks links = null;
Expand Down
35 changes: 30 additions & 5 deletions src/JsonApiDotNetCore/Services/DefaultResourceService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using JsonApiDotNetCore.Internal.Contracts;
using JsonApiDotNetCore.Query;
using JsonApiDotNetCore.Extensions;
using System.Collections;

namespace JsonApiDotNetCore.Services
{
Expand Down Expand Up @@ -124,6 +125,7 @@ public virtual async Task<TResource> GetAsync(TId id)
return entity;
}


// triggered by GET /articles/1/relationships/{relationshipName}
public virtual async Task<TResource> GetRelationshipsAsync(TId id, string relationshipName)
{
Expand All @@ -135,15 +137,36 @@ public virtual async Task<TResource> 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<RelationshipAttribute> { relationship });
var baseQuery = _repository.Get(id);
var entityQuery = ApplyInclude(baseQuery, chainPrefix: new List<RelationshipAttribute> { relationship });

var entity = await _repository.FirstOrDefaultAsync(entityQuery);
if (entity == null)


// lol
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unclear comment

var relationshipValue = typeof(TResource).GetProperty(relationship.InternalRelationshipName).GetValue(entity) ;
var relEmpty = relationshipValue == null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable


if(relationshipValue == 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.");
return null;
}
var listCast = (IList) relationshipValue;
if(listCast != null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expression is always true. And should test for IEnumerable instead of IList.

{
if(listCast.Count == 0)
{
return null;
}
}

//if (entity == null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this block commented out? Should it be removed instead?

//{
// /// 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.
_hookExecutor.AfterRead(AsList(entity), ResourcePipeline.GetRelationship);
Expand Down Expand Up @@ -339,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;
}

Expand Down
Loading