From 10184acc2c1413999b5cdecb07bfef9c779f0f56 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 15:32:07 -0500 Subject: [PATCH 1/7] refactor(jsonapi-exception): treat all errors as error collection add overloads for throwing errors directly --- .../Internal/JsonApiException.cs | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/JsonApiDotNetCore/Internal/JsonApiException.cs b/src/JsonApiDotNetCore/Internal/JsonApiException.cs index e5ecb56fd1..30b35770fa 100644 --- a/src/JsonApiDotNetCore/Internal/JsonApiException.cs +++ b/src/JsonApiDotNetCore/Internal/JsonApiException.cs @@ -4,28 +4,34 @@ namespace JsonApiDotNetCore.Internal { public class JsonApiException : Exception { - private string _statusCode; - private string _detail; - private string _message; + private ErrorCollection _errors = new ErrorCollection(); + + public JsonApiException(ErrorCollection errorCollection) + { + _errors = errorCollection; + } + + public JsonApiException(Error error) + : base(error.Title) + { + _errors.Add(error); + } public JsonApiException(string statusCode, string message) : base(message) { - _statusCode = statusCode; - _message = message; + _errors.Add(new Error(statusCode, message, null)); } public JsonApiException(string statusCode, string message, string detail) : base(message) { - _statusCode = statusCode; - _message = message; - _detail = detail; + _errors.Add(new Error(statusCode, message, detail)); } - public Error GetError() + public ErrorCollection GetError() { - return new Error(_statusCode, _message, _detail); + return _errors; } } } From 2feec1d30a5fc940b473e8d6d76d3a0b476f0cbb Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 15:33:05 -0500 Subject: [PATCH 2/7] feat(jsonapi-exception): add logic for getting error collectoon status --- src/JsonApiDotNetCore/Internal/Error.cs | 2 ++ .../Internal/JsonApiException.cs | 15 +++++++++++++++ .../Middleware/JsonApiExceptionFilter.cs | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Internal/Error.cs b/src/JsonApiDotNetCore/Internal/Error.cs index 01c4a26de0..a7aca41962 100644 --- a/src/JsonApiDotNetCore/Internal/Error.cs +++ b/src/JsonApiDotNetCore/Internal/Error.cs @@ -28,5 +28,7 @@ public Error(string status, string title, string detail) [JsonProperty("status")] public string Status { get; set; } + + public int StatusCode { get { return int.Parse(Status); } } } } diff --git a/src/JsonApiDotNetCore/Internal/JsonApiException.cs b/src/JsonApiDotNetCore/Internal/JsonApiException.cs index 30b35770fa..907b1db7fd 100644 --- a/src/JsonApiDotNetCore/Internal/JsonApiException.cs +++ b/src/JsonApiDotNetCore/Internal/JsonApiException.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; namespace JsonApiDotNetCore.Internal { @@ -33,5 +34,19 @@ public ErrorCollection GetError() { return _errors; } + + public int GetStatusCode() + { + if(_errors.Errors.Count == 1) + return _errors.Errors[0].StatusCode; + + if(_errors.Errors.FirstOrDefault(e => e.StatusCode >= 500) != null) + return 500; + + if(_errors.Errors.FirstOrDefault(e => e.StatusCode >= 400) != null) + return 400; + + return 500; + } } } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs index 479a947e5e..ee038e7902 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs @@ -23,7 +23,7 @@ public void OnException(ExceptionContext context) var error = jsonApiException.GetError(); var result = new ObjectResult(error); - result.StatusCode = Convert.ToInt16(error.Status); + result.StatusCode = jsonApiException.GetStatusCode(); context.Result = result; } } From c459700a92911c0d304756d18afe04425ca6d44e Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 16:02:00 -0500 Subject: [PATCH 3/7] fix(error): ignore StatusCode property --- src/JsonApiDotNetCore/Internal/Error.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/JsonApiDotNetCore/Internal/Error.cs b/src/JsonApiDotNetCore/Internal/Error.cs index a7aca41962..b9261324a5 100644 --- a/src/JsonApiDotNetCore/Internal/Error.cs +++ b/src/JsonApiDotNetCore/Internal/Error.cs @@ -29,6 +29,7 @@ public Error(string status, string title, string detail) [JsonProperty("status")] public string Status { get; set; } + [JsonIgnore] public int StatusCode { get { return int.Parse(Status); } } } } From 75e72b7824fc8e1f3d013c2e977cbf341f01cd22 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 16:03:47 -0500 Subject: [PATCH 4/7] refactor(writer): move logic into serializer --- .../Formatters/JsonApiWriter.cs | 39 +------------ .../Serialization/JsonApiSerializer.cs | 55 +++++++++++++++++-- 2 files changed, 52 insertions(+), 42 deletions(-) diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs b/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs index 8607026387..730a88f13e 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs @@ -2,26 +2,20 @@ using System.Text; using System.Threading.Tasks; using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; namespace JsonApiDotNetCore.Formatters { public class JsonApiWriter : IJsonApiWriter { private readonly ILogger _logger; - private readonly IJsonApiContext _jsonApiContext; private readonly IJsonApiSerializer _serializer; - public JsonApiWriter(IJsonApiContext jsonApiContext, - IJsonApiSerializer serializer, + public JsonApiWriter(IJsonApiSerializer serializer, ILoggerFactory loggerFactory) { - _jsonApiContext = jsonApiContext; _serializer = serializer; _logger = loggerFactory.CreateLogger(); } @@ -58,36 +52,7 @@ public async Task WriteAsync(OutputFormatterWriteContext context) private string GetResponseBody(object responseObject) { - if (responseObject == null) - return GetNullDataResponse(); - - if (responseObject.GetType() == typeof(Error) || _jsonApiContext.RequestEntity == null) - return GetErrorJson(responseObject, _logger); - return _serializer.Serialize(responseObject); - } - - private string GetNullDataResponse() - { - return JsonConvert.SerializeObject(new Document - { - Data = null - }); - } - - private string GetErrorJson(object responseObject, ILogger logger) - { - if (responseObject.GetType() == typeof(Error)) - { - var errors = new ErrorCollection(); - errors.Add((Error)responseObject); - return errors.GetJson(); - } - else - { - logger?.LogInformation("Response was not a JSONAPI entity. Serializing as plain JSON."); - return JsonConvert.SerializeObject(responseObject); - } - } + } } } \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs index 58bc9b19d0..e4fb140d5b 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiSerializer.cs @@ -1,6 +1,9 @@ using System.Collections.Generic; using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace JsonApiDotNetCore.Serialization @@ -8,28 +11,70 @@ namespace JsonApiDotNetCore.Serialization public class JsonApiSerializer : IJsonApiSerializer { private readonly IDocumentBuilder _documentBuilder; + private readonly ILogger _logger; + private readonly IJsonApiContext _jsonApiContext; - public JsonApiSerializer(IDocumentBuilder documentBuilder) + public JsonApiSerializer( + IJsonApiContext jsonApiContext, + IDocumentBuilder documentBuilder) { + _jsonApiContext = jsonApiContext; + _documentBuilder = documentBuilder; + } + + public JsonApiSerializer( + IJsonApiContext jsonApiContext, + IDocumentBuilder documentBuilder, + ILoggerFactory loggerFactory) + { + _jsonApiContext = jsonApiContext; _documentBuilder = documentBuilder; + _logger = loggerFactory?.CreateLogger(); } public string Serialize(object entity) { + if (entity == null) + return GetNullDataResponse(); + + if (entity.GetType() == typeof(ErrorCollection) || _jsonApiContext.RequestEntity == null) + return GetErrorJson(entity, _logger); + if (entity is IEnumerable) - return _serializeDocuments(entity); + return SerializeDocuments(entity); + + return SerializeDocument(entity); + } + + private string GetNullDataResponse() + { + return JsonConvert.SerializeObject(new Document + { + Data = null + }); + } - return _serializeDocument(entity); + private string GetErrorJson(object responseObject, ILogger logger) + { + if (responseObject is ErrorCollection errorCollection) + { + return errorCollection.GetJson(); + } + else + { + logger?.LogInformation("Response was not a JSONAPI entity. Serializing as plain JSON."); + return JsonConvert.SerializeObject(responseObject); + } } - private string _serializeDocuments(object entity) + private string SerializeDocuments(object entity) { var entities = entity as IEnumerable; var documents = _documentBuilder.Build(entities); return _serialize(documents); } - private string _serializeDocument(object entity) + private string SerializeDocument(object entity) { var identifiableEntity = entity as IIdentifiable; var document = _documentBuilder.Build(identifiableEntity); From 3365d59c6d0cabe68e517fe5ab4dac74d144c48f Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 16:04:04 -0500 Subject: [PATCH 5/7] fix(error-collection): use camel-case serialization --- src/JsonApiDotNetCore/Internal/ErrorCollection.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Internal/ErrorCollection.cs b/src/JsonApiDotNetCore/Internal/ErrorCollection.cs index 6e5c375da1..bf0375843d 100644 --- a/src/JsonApiDotNetCore/Internal/ErrorCollection.cs +++ b/src/JsonApiDotNetCore/Internal/ErrorCollection.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Internal { @@ -20,7 +21,8 @@ public void Add(Error error) public string GetJson() { return JsonConvert.SerializeObject(this, new JsonSerializerSettings { - NullValueHandling = NullValueHandling.Ignore + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = new CamelCasePropertyNamesContractResolver() }); } } From 70b273a6ab90901d307013b45b73e0128005fb3c Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 16:04:30 -0500 Subject: [PATCH 6/7] test(extensibility): verify users can serialize custom errors --- .../Extensibility/CustomErrorTests.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs new file mode 100644 index 0000000000..3ba7d18156 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs @@ -0,0 +1,53 @@ +using DotNetCoreDocs; +using JsonApiDotNetCoreExample; +using DotNetCoreDocs.Writers; +using Newtonsoft.Json; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Serialization; +using Xunit; +using System.Diagnostics; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility +{ + public class CustomErrorTests + { + [Fact] + public void Can_Return_Custom_Error_Types() + { + // while(!Debugger.IsAttached) { bool stop = false; } + + // arrange + var error = new CustomError("507", "title", "detail", "custom"); + var errorCollection = new ErrorCollection(); + errorCollection.Add(error); + + var expectedJson = JsonConvert.SerializeObject(new { + errors = new dynamic[] { + new { + myCustomProperty = "custom", + title = "title", + detail = "detail", + status = "507" + } + } + }); + + // act + var result = new JsonApiSerializer(null, null, null) + .Serialize(errorCollection); + + // assert + Assert.Equal(expectedJson, result); + + } + + class CustomError : Error { + public CustomError(string status, string title, string detail, string myProp) + : base(status, title, detail) + { + MyCustomProperty = myProp; + } + public string MyCustomProperty { get; set; } + } + } +} From 7fd3e48aa68c2e915d4b78555d3751de8e4bd53d Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 16:13:51 -0500 Subject: [PATCH 7/7] docs(readme): document custom error usage --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index 29b8bff318..fc79478e28 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ JsonApiDotnetCore provides a framework for building [json:api](http://jsonapi.or - [Sorting](#sorting) - [Meta](#meta) - [Client Generated Ids](#client-generated-ids) + - [Custom Errors](#custom-errors) - [Tests](#tests) ## Comprehensive Demo @@ -364,6 +365,38 @@ services.AddJsonApi(opt => }); ``` +### Custom Errors + +By default, errors will only contain the properties defined by the internal [Error](https://github.com/Research-Institute/json-api-dotnet-core/blob/master/src/JsonApiDotNetCore/Internal/Error.cs) class. However, you can create your own by inheriting from `Error` and either throwing it in a `JsonApiException` or returning the error from your controller. + +```chsarp +// custom error definition +public class CustomError : Error { + public CustomError(string status, string title, string detail, string myProp) + : base(status, title, detail) + { + MyCustomProperty = myProp; + } + public string MyCustomProperty { get; set; } +} + +// throwing a custom error +public void MyMethod() { + var error = new CustomError("507", "title", "detail", "custom"); + throw new JsonApiException(error); +} + +// returning from controller +[HttpPost] +public override async Task PostAsync([FromBody] MyEntity entity) +{ + if(_db.IsFull) + return new ObjectResult(new CustomError("507", "Database is full.", "Theres no more room.", "Sorry.")); + + // ... +} +``` + ## Tests I am using DotNetCoreDocs to generate sample requests and documentation.