Skip to content

Error handling and logging #714

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 60 commits into from
Apr 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
b894993
Made some types which are not intended to be instantiated abstract
Mar 25, 2020
07b9f1d
Added members to Error as described at https://jsonapi.org/format/#er…
Mar 25, 2020
726a8e8
Replaced numeric HTTP status codes with HttpStatusCode enum
Mar 30, 2020
00c6af8
Expose HttpStatusCode on Error object
Mar 30, 2020
10afd16
Replaced static error class with custom exception
Mar 30, 2020
eb76b0d
Moved Error type to non-internal namespace
Mar 30, 2020
940bb1d
Cleanup errors produced from model state validation
Mar 30, 2020
8660887
Renamed ErrorCollection to ErrorDocument, because it *contains* a col…
Mar 30, 2020
4f44a19
Removed custom error object; made json:api objects sealed
Mar 30, 2020
99c2502
Removed ErrorSource from constructors and options, because it is almo…
Mar 31, 2020
bc89762
Added error documentation from json:api spec.
Mar 31, 2020
f7f58db
Changed Error.Meta to contain multiple key/value pairs. Replaced stat…
Mar 31, 2020
f9ad031
Cleanup Error constructor overloads (only accept required parameter)
Mar 31, 2020
16c1448
Fixed broken modelstate validation; converted unit tests into TestSer…
Apr 1, 2020
e0012fd
Major rewrite for error handling:
Apr 1, 2020
602adad
Fixed: use enum instead of magic numbers
Apr 2, 2020
ef88938
Fixed: setting MaxLength on attribute is picked up by EF Core and tra…
Apr 2, 2020
03117a2
Fixed spelling error in comment
Apr 2, 2020
46d23e0
Changes on the flow of parsing query strings, in order to throw bette…
Apr 2, 2020
a6fef14
Query strings: moved the check for empty value up the call stack, cre…
Apr 2, 2020
a6302d9
Removed wrong usage of UnauthorizedAccessException: "The exception th…
Apr 3, 2020
2a24757
Moved exceptions into public namespace
Apr 3, 2020
d57ff7a
Added ObjectCreationException with a unit test. Redirected parameterl…
Apr 3, 2020
1a7130e
Added exception + test for resource type mismatch between endpoint ur…
Apr 3, 2020
3bc5bc6
Wrap errors returned from ActionResults, to help being json:api compl…
Apr 3, 2020
f66c7ee
Fixed: respect casing convention when serializing error responses
Apr 3, 2020
b688192
Another conversion from generic exception to typed exception + test
Apr 3, 2020
fc9c295
Reuse error for method not allowed + unit tests
Apr 3, 2020
c7ce364
Added test for using DisableQueryAttribute
Apr 3, 2020
a84d15f
Added AttributeUsage on attributes
Apr 3, 2020
2f920da
Better messages and tests for more query string errors
Apr 3, 2020
80cd4bf
Fixed copy/paste error
Apr 4, 2020
2024d51
More query string exception usage with tests
Apr 4, 2020
cd79fa5
Updated comments
Apr 4, 2020
08c4266
More cleanup of errors
Apr 4, 2020
52ee7b8
More converted exceptions with tests
Apr 4, 2020
a99cc59
Reverted some exceptions because there are not user-facing but helpin…
Apr 6, 2020
7c40384
Fixed: broke dependency between action results and json:api error doc…
Apr 6, 2020
9676990
Fixed: use status code from error; unwrap reflection errors
Apr 6, 2020
6fae6f8
Tweaks in error logging
Apr 6, 2020
d92b3b9
Include request body in logged exception when available
Apr 6, 2020
f0df15b
More assertions on non-success status codes and exceptions. Removed e…
Apr 6, 2020
d7a81d0
Removed exception for unable to serialize response
Apr 6, 2020
0d5b897
Lowered log level in example
Apr 7, 2020
cad4834
Tweaks in log formatting
Apr 7, 2020
6c2a659
Added logging for query string parsing
Apr 7, 2020
d5dbf72
Added JSON request/response logging
Apr 7, 2020
674668e
Added trace-level logging for the controller > service > repository c…
Apr 7, 2020
3697967
Fixed: allow unpaged result when no maximum page size is set
Apr 7, 2020
5680190
Fixed: duplicate tests
Apr 7, 2020
e553070
separate constructors (easier to derive)
Apr 7, 2020
8f96c22
Replaced ActionResults with rich exceptions
Apr 7, 2020
a5dddad
Removed unused usings
Apr 7, 2020
f889c90
Various small fixes
Apr 7, 2020
2b65e32
Fixed: update file name to match with class
Apr 7, 2020
1439d4f
Added tests for using ActionResult without [ApiController]
Apr 7, 2020
ef8d650
Fixed: De-duplicate field names in sparse fieldset; do not ignore case
Apr 8, 2020
e0a139b
Removed more case-insensitive string comparisons. They are tricky, be…
Apr 8, 2020
6969ce3
Inlined casing conversions
Apr 8, 2020
05a9dbe
More casing-related updates
Apr 8, 2020
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
5 changes: 3 additions & 2 deletions benchmarks/Query/QueryParserBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using JsonApiDotNetCore.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging.Abstractions;

namespace Benchmarks.Query
{
Expand Down Expand Up @@ -44,7 +45,7 @@ private static QueryParameterParser CreateQueryParameterDiscoveryForSort(IResour
sortService
};

return new QueryParameterParser(options, queryStringAccessor, queryServices);
return new QueryParameterParser(options, queryStringAccessor, queryServices, NullLoggerFactory.Instance);
}

private static QueryParameterParser CreateQueryParameterDiscoveryForAll(IResourceGraph resourceGraph,
Expand All @@ -65,7 +66,7 @@ private static QueryParameterParser CreateQueryParameterDiscoveryForAll(IResourc
omitNullService
};

return new QueryParameterParser(options, queryStringAccessor, queryServices);
return new QueryParameterParser(options, queryStringAccessor, queryServices, NullLoggerFactory.Instance);
}

[Benchmark]
Expand Down
3 changes: 2 additions & 1 deletion benchmarks/Serialization/JsonApiSerializerBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using BenchmarkDotNet.Attributes;
using JsonApiDotNetCore.Graph;
using JsonApiDotNetCore.Internal.Contracts;
using JsonApiDotNetCore.Managers;
using JsonApiDotNetCore.Query;
Expand Down Expand Up @@ -33,7 +34,7 @@ public JsonApiSerializerBenchmarks()
var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, new ResourceObjectBuilderSettings());

_jsonApiSerializer = new ResponseSerializer<BenchmarkResource>(metaBuilderMock.Object, linkBuilderMock.Object,
includeBuilderMock.Object, fieldsToSerialize, resourceObjectBuilder);
includeBuilderMock.Object, fieldsToSerialize, resourceObjectBuilder, new CamelCaseFormatter());
}

private static FieldsToSerialize CreateFieldsToSerialize(IResourceGraph resourceGraph)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

namespace JsonApiDotNetCoreExample.Controllers
{
[DisableQuery(StandardQueryStringParameters.Sort | StandardQueryStringParameters.Page)]
public sealed class ArticlesController : JsonApiController<Article>
{
public ArticlesController(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

namespace JsonApiDotNetCoreExample.Controllers
{
[DisableQuery("skipCache")]
public sealed class TagsController : JsonApiController<Tag>
{
public TagsController(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Controllers;
using JsonApiDotNetCore.Services;
using JsonApiDotNetCoreExample.Models;
using Microsoft.Extensions.Logging;

namespace JsonApiDotNetCoreExample.Controllers
{
public sealed class ThrowingResourcesController : JsonApiController<ThrowingResource>
{
public ThrowingResourcesController(
IJsonApiOptions jsonApiOptions,
ILoggerFactory loggerFactory,
IResourceService<ThrowingResource> resourceService)
: base(jsonApiOptions, loggerFactory, resourceService)
{ }
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Controllers;
using JsonApiDotNetCore.Exceptions;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Services;
using JsonApiDotNetCoreExample.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace JsonApiDotNetCoreExample.Controllers
{
[ApiController]
[DisableRoutingConvention, Route("custom/route/todoItems")]
public class TodoItemsCustomController : CustomJsonApiController<TodoItem>
{
public TodoItemsCustomController(
IJsonApiOptions options,
IResourceService<TodoItem> resourceService,
ILoggerFactory loggerFactory)
: base(options, resourceService, loggerFactory)
IResourceService<TodoItem> resourceService)
: base(options, resourceService)
{ }
}

Expand All @@ -26,8 +27,7 @@ public class CustomJsonApiController<T>
{
public CustomJsonApiController(
IJsonApiOptions options,
IResourceService<T, int> resourceService,
ILoggerFactory loggerFactory)
IResourceService<T, int> resourceService)
: base(options, resourceService)
{
}
Expand All @@ -41,7 +41,7 @@ public class CustomJsonApiController<T, TId>

private IActionResult Forbidden()
{
return new StatusCodeResult(403);
return new StatusCodeResult((int)HttpStatusCode.Forbidden);
}

public CustomJsonApiController(
Expand All @@ -68,22 +68,29 @@ public async Task<IActionResult> GetAsync()
[HttpGet("{id}")]
public async Task<IActionResult> GetAsync(TId id)
{
var entity = await _resourceService.GetAsync(id);

if (entity == null)
try
{
var entity = await _resourceService.GetAsync(id);
return Ok(entity);
}
catch (ResourceNotFoundException)
{
return NotFound();

return Ok(entity);
}
}

[HttpGet("{id}/relationships/{relationshipName}")]
public async Task<IActionResult> GetRelationshipsAsync(TId id, string relationshipName)
{
var relationship = _resourceService.GetRelationshipAsync(id, relationshipName);
if (relationship == null)
try
{
var relationship = await _resourceService.GetRelationshipsAsync(id, relationshipName);
return Ok(relationship);
}
catch (ResourceNotFoundException)
{
return NotFound();

return await GetRelationshipAsync(id, relationshipName);
}
}

[HttpGet("{id}/{relationshipName}")]
Expand Down Expand Up @@ -113,12 +120,15 @@ public async Task<IActionResult> PatchAsync(TId id, [FromBody] T entity)
if (entity == null)
return UnprocessableEntity();

var updatedEntity = await _resourceService.UpdateAsync(id, entity);

if (updatedEntity == null)
try
{
var updatedEntity = await _resourceService.UpdateAsync(id, entity);
return Ok(updatedEntity);
}
catch (ResourceNotFoundException)
{
return NotFound();

return Ok(updatedEntity);
}
}

[HttpPatch("{id}/relationships/{relationshipName}")]
Expand All @@ -131,11 +141,7 @@ public async Task<IActionResult> PatchRelationshipsAsync(TId id, string relation
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteAsync(TId id)
{
var wasDeleted = await _resourceService.DeleteAsync(id);

if (!wasDeleted)
return NotFound();

await _resourceService.DeleteAsync(id);
return NoContent();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System.Net;
using System.Threading.Tasks;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Controllers;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Models.JsonApiDocuments;
using JsonApiDotNetCore.Services;
using JsonApiDotNetCoreExample.Models;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -9,7 +12,7 @@
namespace JsonApiDotNetCoreExample.Controllers
{
public abstract class AbstractTodoItemsController<T>
: JsonApiController<T> where T : class, IIdentifiable<int>
: BaseJsonApiController<T> where T : class, IIdentifiable<int>
{
protected AbstractTodoItemsController(
IJsonApiOptions jsonApiOptions,
Expand All @@ -19,6 +22,7 @@ protected AbstractTodoItemsController(
{ }
}

[DisableRoutingConvention]
[Route("/abstract")]
public class TodoItemsTestController : AbstractTodoItemsController<TodoItem>
{
Expand All @@ -28,5 +32,49 @@ public TodoItemsTestController(
IResourceService<TodoItem> service)
: base(jsonApiOptions, loggerFactory, service)
{ }

[HttpGet]
public override async Task<IActionResult> GetAsync() => await base.GetAsync();

[HttpGet("{id}")]
public override async Task<IActionResult> GetAsync(int id) => await base.GetAsync(id);

[HttpGet("{id}/relationships/{relationshipName}")]
public override async Task<IActionResult> GetRelationshipsAsync(int id, string relationshipName)
=> await base.GetRelationshipsAsync(id, relationshipName);

[HttpGet("{id}/{relationshipName}")]
public override async Task<IActionResult> GetRelationshipAsync(int id, string relationshipName)
=> await base.GetRelationshipAsync(id, relationshipName);

[HttpPost]
public override async Task<IActionResult> PostAsync(TodoItem entity)
{
await Task.Yield();

return NotFound(new Error(HttpStatusCode.NotFound)
{
Title = "NotFound ActionResult with explicit error object."
});
}

[HttpPatch("{id}")]
public override async Task<IActionResult> PatchAsync(int id, [FromBody] TodoItem entity)
{
return await base.PatchAsync(id, entity);
}

[HttpPatch("{id}/relationships/{relationshipName}")]
public override async Task<IActionResult> PatchRelationshipsAsync(
int id, string relationshipName, [FromBody] object relationships)
=> await base.PatchRelationshipsAsync(id, relationshipName, relationships);

[HttpDelete("{id}")]
public override async Task<IActionResult> DeleteAsync(int id)
{
await Task.Yield();

return NotFound();
}
}
}
1 change: 1 addition & 0 deletions src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public sealed class AppDbContext : DbContext
public DbSet<ArticleTag> ArticleTags { get; set; }
public DbSet<IdentifiableArticleTag> IdentifiableArticleTags { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<ThrowingResource> ThrowingResources { get; set; }

public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

Expand Down
4 changes: 3 additions & 1 deletion src/Examples/JsonApiDotNetCoreExample/Models/Tag.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System.ComponentModel.DataAnnotations;
using JsonApiDotNetCore.Models;

namespace JsonApiDotNetCoreExample.Models
{
public class Tag : Identifiable
{
[Attr]
[RegularExpression(@"^\W$")]
public string Name { get; set; }
}
}
}
29 changes: 29 additions & 0 deletions src/Examples/JsonApiDotNetCoreExample/Models/ThrowingResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Diagnostics;
using System.Linq;
using JsonApiDotNetCore.Formatters;
using JsonApiDotNetCore.Models;

namespace JsonApiDotNetCoreExample.Models
{
public sealed class ThrowingResource : Identifiable
{
[Attr]
public string FailsOnSerialize
{
get
{
var isSerializingResponse = new StackTrace().GetFrames()
.Any(frame => frame.GetMethod().DeclaringType == typeof(JsonApiWriter));

if (isSerializingResponse)
{
throw new InvalidOperationException($"The value for the '{nameof(FailsOnSerialize)}' property is currently unavailable.");
}

return string.Empty;
}
set { }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchBrowser": false,
"launchUrl": "api/values",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"JsonApiDotNetCoreExample": {
"commandName": "Project",
"launchBrowser": true,
"launchBrowser": false,
"launchUrl": "http://localhost:5000/api/values",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5000/"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using System.Collections.Generic;
using System.Linq;
using System;
using JsonApiDotNetCore.Internal;
using System.Net;
using JsonApiDotNetCore.Exceptions;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Hooks;
using JsonApiDotNetCoreExample.Models;
using JsonApiDotNetCore.Internal.Contracts;
using JsonApiDotNetCore.Models.JsonApiDocuments;

namespace JsonApiDotNetCoreExample.Resources
{
Expand All @@ -17,9 +18,13 @@ public override IEnumerable<Article> OnReturn(HashSet<Article> entities, Resourc
{
if (pipeline == ResourcePipeline.GetSingle && entities.Single().Name == "Classified")
{
throw new JsonApiException(403, "You are not allowed to see this article!", new UnauthorizedAccessException());
throw new JsonApiException(new Error(HttpStatusCode.Forbidden)
{
Title = "You are not allowed to see this article."
});
}
return entities.Where(t => t.Name != "This should be not be included");

return entities.Where(t => t.Name != "This should not be included");
}
}
}
Expand Down
Loading