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. 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/Internal/Error.cs b/src/JsonApiDotNetCore/Internal/Error.cs index 01c4a26de0..b9261324a5 100644 --- a/src/JsonApiDotNetCore/Internal/Error.cs +++ b/src/JsonApiDotNetCore/Internal/Error.cs @@ -28,5 +28,8 @@ public Error(string status, string title, string detail) [JsonProperty("status")] public string Status { get; set; } + + [JsonIgnore] + public int StatusCode { get { return int.Parse(Status); } } } } 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() }); } } diff --git a/src/JsonApiDotNetCore/Internal/JsonApiException.cs b/src/JsonApiDotNetCore/Internal/JsonApiException.cs index e5ecb56fd1..907b1db7fd 100644 --- a/src/JsonApiDotNetCore/Internal/JsonApiException.cs +++ b/src/JsonApiDotNetCore/Internal/JsonApiException.cs @@ -1,31 +1,52 @@ using System; +using System.Linq; 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; + } + + 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; } } 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); 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; } + } + } +}