Skip to content

Refactored JsonApiException to contain a list of errors. #894

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

Merged
merged 1 commit into from
Nov 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions src/JsonApiDotNetCore/Errors/IHasMultipleErrors.cs

This file was deleted.

16 changes: 7 additions & 9 deletions src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,21 @@ namespace JsonApiDotNetCore.Errors
/// <summary>
/// The error that is thrown when model state validation fails.
/// </summary>
public class InvalidModelStateException : Exception, IHasMultipleErrors
public class InvalidModelStateException : JsonApiException
{
public IReadOnlyCollection<Error> Errors { get; }

public InvalidModelStateException(ModelStateDictionary modelState, Type resourceType,
bool includeExceptionStackTraceInErrors, NamingStrategy namingStrategy)
: base(FromModelState(modelState, resourceType, includeExceptionStackTraceInErrors, namingStrategy))
{
if (modelState == null) throw new ArgumentNullException(nameof(modelState));
if (resourceType == null) throw new ArgumentNullException(nameof(resourceType));
if (namingStrategy == null) throw new ArgumentNullException(nameof(namingStrategy));

Errors = FromModelState(modelState, resourceType, includeExceptionStackTraceInErrors, namingStrategy);
}

private static IReadOnlyCollection<Error> FromModelState(ModelStateDictionary modelState, Type resourceType,
bool includeExceptionStackTraceInErrors, NamingStrategy namingStrategy)
{
if (modelState == null) throw new ArgumentNullException(nameof(modelState));
if (resourceType == null) throw new ArgumentNullException(nameof(resourceType));
if (namingStrategy == null) throw new ArgumentNullException(nameof(namingStrategy));

List<Error> errors = new List<Error>();

foreach (var (propertyName, entry) in modelState.Where(x => x.Value.Errors.Any()))
Expand All @@ -40,7 +38,7 @@ private static IReadOnlyCollection<Error> FromModelState(ModelStateDictionary mo
{
if (modelError.Exception is JsonApiException jsonApiException)
{
errors.Add(jsonApiException.Error);
errors.AddRange(jsonApiException.Errors);
}
else
{
Expand Down
27 changes: 22 additions & 5 deletions src/JsonApiDotNetCore/Errors/JsonApiException.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JsonApiDotNetCore.Serialization.Objects;
using Newtonsoft.Json;

namespace JsonApiDotNetCore.Errors
{
/// <summary>
/// The base class for an <see cref="Exception"/> that represents a json:api error object in an unsuccessful response.
/// The base class for an <see cref="Exception"/> that represents one or more json:api error objects in an unsuccessful response.
/// </summary>
public class JsonApiException : Exception
{
Expand All @@ -15,14 +17,29 @@ public class JsonApiException : Exception
Formatting = Formatting.Indented
};

public Error Error { get; }
public IReadOnlyList<Error> Errors { get; }

public JsonApiException(Error error, Exception innerException = null)
: base(error.Title, innerException)
: base(null, innerException)
{
Error = error;
if (error == null) throw new ArgumentNullException(nameof(error));

Errors = new[] {error};
}

public JsonApiException(IEnumerable<Error> errors, Exception innerException = null)
: base(null, innerException)
{
if (errors == null) throw new ArgumentNullException(nameof(errors));

Errors = errors.ToList();

if (!Errors.Any())
{
throw new ArgumentException("At least one error is required.", nameof(errors));
}
}

public override string Message => "Error = " + JsonConvert.SerializeObject(Error, _errorSerializerSettings);
public override string Message => "Errors = " + JsonConvert.SerializeObject(Errors, _errorSerializerSettings);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,14 @@ namespace JsonApiDotNetCore.Errors
/// <summary>
/// The error that is thrown when referencing one or more non-existing resources in one or more relationships.
/// </summary>
public sealed class ResourcesInRelationshipsNotFoundException : Exception, IHasMultipleErrors
public sealed class ResourcesInRelationshipsNotFoundException : JsonApiException
{
public IReadOnlyCollection<Error> Errors { get; }

public ResourcesInRelationshipsNotFoundException(IEnumerable<MissingResourceInRelationship> missingResources)
: base(missingResources.Select(CreateError))
{
Errors = missingResources.Select(CreateError).ToList();
}

private Error CreateError(MissingResourceInRelationship missingResourceInRelationship)
private static Error CreateError(MissingResourceInRelationship missingResourceInRelationship)
{
return new Error(HttpStatusCode.NotFound)
{
Expand Down
38 changes: 20 additions & 18 deletions src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ protected virtual LogLevel GetLogLevel(Exception exception)
return LogLevel.None;
}

if (exception is JsonApiException || exception is InvalidModelStateException)
if (exception is JsonApiException)
{
return LogLevel.Information;
}
Expand All @@ -63,36 +63,38 @@ protected virtual string GetLogMessage(Exception exception)
{
if (exception == null) throw new ArgumentNullException(nameof(exception));

return exception is JsonApiException jsonApiException
? jsonApiException.Error.Title
: exception.Message;
return exception.Message;
}

protected virtual ErrorDocument CreateErrorDocument(Exception exception)
{
if (exception == null) throw new ArgumentNullException(nameof(exception));

if (exception is IHasMultipleErrors exceptionWithMultipleErrors)
{
return new ErrorDocument(exceptionWithMultipleErrors.Errors);
}

Error error = exception is JsonApiException jsonApiException
? jsonApiException.Error
var errors = exception is JsonApiException jsonApiException
? jsonApiException.Errors
: exception is TaskCanceledException
? new Error((HttpStatusCode) 499)
? new[]
{
Title = "Request execution was canceled."
new Error((HttpStatusCode) 499)
{
Title = "Request execution was canceled."
}
}
: new Error(HttpStatusCode.InternalServerError)
: new[]
{
Title = "An unhandled error occurred while processing this request.",
Detail = exception.Message
new Error(HttpStatusCode.InternalServerError)
{
Title = "An unhandled error occurred while processing this request.",
Detail = exception.Message
}
};

ApplyOptions(error, exception);
foreach (var error in errors)
{
ApplyOptions(error, exception);
}

return new ErrorDocument(error);
return new ErrorDocument(errors);
}

private void ApplyOptions(Error error, Exception exception)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ protected override ErrorDocument CreateErrorDocument(Exception exception)
{
if (exception is NoPermissionException noPermissionException)
{
noPermissionException.Error.Meta.Data.Add("support",
noPermissionException.Errors[0].Meta.Data.Add("support",
"For support, email to: support@company.com?subject=" + noPermissionException.CustomerCode);
}

Expand Down
14 changes: 7 additions & 7 deletions test/UnitTests/Controllers/BaseJsonApiController_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public async Task GetAsync_Throws_405_If_No_Service()
var exception = await Assert.ThrowsAsync<RequestMethodNotAllowedException>(() => controller.GetAsync(CancellationToken.None));

// Assert
Assert.Equal(HttpStatusCode.MethodNotAllowed, exception.Error.StatusCode);
Assert.Equal(HttpStatusCode.MethodNotAllowed, exception.Errors[0].StatusCode);
Assert.Equal(HttpMethod.Get, exception.Method);
}

Expand Down Expand Up @@ -106,7 +106,7 @@ public async Task GetAsyncById_Throws_405_If_No_Service()
var exception = await Assert.ThrowsAsync<RequestMethodNotAllowedException>(() => controller.GetAsync(id, CancellationToken.None));

// Assert
Assert.Equal(HttpStatusCode.MethodNotAllowed, exception.Error.StatusCode);
Assert.Equal(HttpStatusCode.MethodNotAllowed, exception.Errors[0].StatusCode);
Assert.Equal(HttpMethod.Get, exception.Method);
}

Expand Down Expand Up @@ -136,7 +136,7 @@ public async Task GetRelationshipsAsync_Throws_405_If_No_Service()
var exception = await Assert.ThrowsAsync<RequestMethodNotAllowedException>(() => controller.GetRelationshipAsync(id, string.Empty, CancellationToken.None));

// Assert
Assert.Equal(HttpStatusCode.MethodNotAllowed, exception.Error.StatusCode);
Assert.Equal(HttpStatusCode.MethodNotAllowed, exception.Errors[0].StatusCode);
Assert.Equal(HttpMethod.Get, exception.Method);
}

Expand Down Expand Up @@ -166,7 +166,7 @@ public async Task GetRelationshipAsync_Throws_405_If_No_Service()
var exception = await Assert.ThrowsAsync<RequestMethodNotAllowedException>(() => controller.GetSecondaryAsync(id, string.Empty, CancellationToken.None));

// Assert
Assert.Equal(HttpStatusCode.MethodNotAllowed, exception.Error.StatusCode);
Assert.Equal(HttpStatusCode.MethodNotAllowed, exception.Errors[0].StatusCode);
Assert.Equal(HttpMethod.Get, exception.Method);
}

Expand Down Expand Up @@ -199,7 +199,7 @@ public async Task PatchAsync_Throws_405_If_No_Service()
var exception = await Assert.ThrowsAsync<RequestMethodNotAllowedException>(() => controller.PatchAsync(id, resource, CancellationToken.None));

// Assert
Assert.Equal(HttpStatusCode.MethodNotAllowed, exception.Error.StatusCode);
Assert.Equal(HttpStatusCode.MethodNotAllowed, exception.Errors[0].StatusCode);
Assert.Equal(HttpMethod.Patch, exception.Method);
}

Expand Down Expand Up @@ -247,7 +247,7 @@ public async Task PatchRelationshipsAsync_Throws_405_If_No_Service()
var exception = await Assert.ThrowsAsync<RequestMethodNotAllowedException>(() => controller.PatchRelationshipAsync(id, string.Empty, null, CancellationToken.None));

// Assert
Assert.Equal(HttpStatusCode.MethodNotAllowed, exception.Error.StatusCode);
Assert.Equal(HttpStatusCode.MethodNotAllowed, exception.Errors[0].StatusCode);
Assert.Equal(HttpMethod.Patch, exception.Method);
}

Expand Down Expand Up @@ -277,7 +277,7 @@ public async Task DeleteAsync_Throws_405_If_No_Service()
var exception = await Assert.ThrowsAsync<RequestMethodNotAllowedException>(() => controller.DeleteAsync(id, CancellationToken.None));

// Assert
Assert.Equal(HttpStatusCode.MethodNotAllowed, exception.Error.StatusCode);
Assert.Equal(HttpStatusCode.MethodNotAllowed, exception.Errors[0].StatusCode);
Assert.Equal(HttpMethod.Delete, exception.Method);
}
}
Expand Down
9 changes: 5 additions & 4 deletions test/UnitTests/QueryStringParameters/DefaultsParseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,11 @@ public void Reader_Read_Fails(string parameterName, string parameterValue, strin
var exception = action.Should().ThrowExactly<InvalidQueryStringParameterException>().And;

exception.QueryParameterName.Should().Be(parameterName);
exception.Error.StatusCode.Should().Be(HttpStatusCode.BadRequest);
exception.Error.Title.Should().Be("The specified defaults is invalid.");
exception.Error.Detail.Should().Be(errorMessage);
exception.Error.Source.Parameter.Should().Be(parameterName);
exception.Errors.Should().HaveCount(1);
exception.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest);
exception.Errors[0].Title.Should().Be("The specified defaults is invalid.");
exception.Errors[0].Detail.Should().Be(errorMessage);
exception.Errors[0].Source.Parameter.Should().Be(parameterName);
}

[Theory]
Expand Down
9 changes: 5 additions & 4 deletions test/UnitTests/QueryStringParameters/FilterParseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,11 @@ public void Reader_Read_Fails(string parameterName, string parameterValue, strin
var exception = action.Should().ThrowExactly<InvalidQueryStringParameterException>().And;

exception.QueryParameterName.Should().Be(parameterName);
exception.Error.StatusCode.Should().Be(HttpStatusCode.BadRequest);
exception.Error.Title.Should().Be("The specified filter is invalid.");
exception.Error.Detail.Should().Be(errorMessage);
exception.Error.Source.Parameter.Should().Be(parameterName);
exception.Errors.Should().HaveCount(1);
exception.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest);
exception.Errors[0].Title.Should().Be("The specified filter is invalid.");
exception.Errors[0].Detail.Should().Be(errorMessage);
exception.Errors[0].Source.Parameter.Should().Be(parameterName);
}

[Theory]
Expand Down
9 changes: 5 additions & 4 deletions test/UnitTests/QueryStringParameters/IncludeParseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,11 @@ public void Reader_Read_Fails(string parameterName, string parameterValue, strin
var exception = action.Should().ThrowExactly<InvalidQueryStringParameterException>().And;

exception.QueryParameterName.Should().Be(parameterName);
exception.Error.StatusCode.Should().Be(HttpStatusCode.BadRequest);
exception.Error.Title.Should().Be("The specified include is invalid.");
exception.Error.Detail.Should().Be(errorMessage);
exception.Error.Source.Parameter.Should().Be(parameterName);
exception.Errors.Should().HaveCount(1);
exception.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest);
exception.Errors[0].Title.Should().Be("The specified include is invalid.");
exception.Errors[0].Detail.Should().Be(errorMessage);
exception.Errors[0].Source.Parameter.Should().Be(parameterName);
}

[Theory]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ public void Reader_Read_Fails(string parameterName, string parameterValue, strin
var exception = action.Should().ThrowExactly<InvalidQueryStringParameterException>().And;

exception.QueryParameterName.Should().Be(parameterName);
exception.Error.StatusCode.Should().Be(HttpStatusCode.BadRequest);
exception.Error.Title.Should().Be("The specified filter is invalid.");
exception.Error.Detail.Should().Be(errorMessage);
exception.Error.Source.Parameter.Should().Be(parameterName);
exception.Errors.Should().HaveCount(1);
exception.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest);
exception.Errors[0].Title.Should().Be("The specified filter is invalid.");
exception.Errors[0].Detail.Should().Be(errorMessage);
exception.Errors[0].Source.Parameter.Should().Be(parameterName);
}

[Theory]
Expand Down
9 changes: 5 additions & 4 deletions test/UnitTests/QueryStringParameters/NullsParseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,11 @@ public void Reader_Read_Fails(string parameterName, string parameterValue, strin
var exception = action.Should().ThrowExactly<InvalidQueryStringParameterException>().And;

exception.QueryParameterName.Should().Be(parameterName);
exception.Error.StatusCode.Should().Be(HttpStatusCode.BadRequest);
exception.Error.Title.Should().Be("The specified nulls is invalid.");
exception.Error.Detail.Should().Be(errorMessage);
exception.Error.Source.Parameter.Should().Be(parameterName);
exception.Errors.Should().HaveCount(1);
exception.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest);
exception.Errors[0].Title.Should().Be("The specified nulls is invalid.");
exception.Errors[0].Detail.Should().Be(errorMessage);
exception.Errors[0].Source.Parameter.Should().Be(parameterName);
}

[Theory]
Expand Down
18 changes: 10 additions & 8 deletions test/UnitTests/QueryStringParameters/PaginationParseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,11 @@ public void Reader_Read_Page_Number_Fails(string parameterValue, string errorMes
var exception = action.Should().ThrowExactly<InvalidQueryStringParameterException>().And;

exception.QueryParameterName.Should().Be("page[number]");
exception.Error.StatusCode.Should().Be(HttpStatusCode.BadRequest);
exception.Error.Title.Should().Be("The specified paging is invalid.");
exception.Error.Detail.Should().Be(errorMessage);
exception.Error.Source.Parameter.Should().Be("page[number]");
exception.Errors.Should().HaveCount(1);
exception.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest);
exception.Errors[0].Title.Should().Be("The specified paging is invalid.");
exception.Errors[0].Detail.Should().Be(errorMessage);
exception.Errors[0].Source.Parameter.Should().Be("page[number]");
}

[Theory]
Expand Down Expand Up @@ -108,10 +109,11 @@ public void Reader_Read_Page_Size_Fails(string parameterValue, string errorMessa
var exception = action.Should().ThrowExactly<InvalidQueryStringParameterException>().And;

exception.QueryParameterName.Should().Be("page[size]");
exception.Error.StatusCode.Should().Be(HttpStatusCode.BadRequest);
exception.Error.Title.Should().Be("The specified paging is invalid.");
exception.Error.Detail.Should().Be(errorMessage);
exception.Error.Source.Parameter.Should().Be("page[size]");
exception.Errors.Should().HaveCount(1);
exception.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest);
exception.Errors[0].Title.Should().Be("The specified paging is invalid.");
exception.Errors[0].Detail.Should().Be(errorMessage);
exception.Errors[0].Source.Parameter.Should().Be("page[size]");
}

[Theory]
Expand Down
9 changes: 5 additions & 4 deletions test/UnitTests/QueryStringParameters/SortParseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,11 @@ public void Reader_Read_Fails(string parameterName, string parameterValue, strin
var exception = action.Should().ThrowExactly<InvalidQueryStringParameterException>().And;

exception.QueryParameterName.Should().Be(parameterName);
exception.Error.StatusCode.Should().Be(HttpStatusCode.BadRequest);
exception.Error.Title.Should().Be("The specified sort is invalid.");
exception.Error.Detail.Should().Be(errorMessage);
exception.Error.Source.Parameter.Should().Be(parameterName);
exception.Errors.Should().HaveCount(1);
exception.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest);
exception.Errors[0].Title.Should().Be("The specified sort is invalid.");
exception.Errors[0].Detail.Should().Be(errorMessage);
exception.Errors[0].Source.Parameter.Should().Be(parameterName);
}

[Theory]
Expand Down
Loading