Skip to content

Commit 8f96c22

Browse files
author
Bart Koelman
committed
Replaced ActionResults with rich exceptions
Fixes in handling of missing relationships + extra tests
1 parent e553070 commit 8f96c22

21 files changed

+461
-121
lines changed

src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
using System.Threading.Tasks;
44
using JsonApiDotNetCore.Configuration;
55
using JsonApiDotNetCore.Controllers;
6+
using JsonApiDotNetCore.Exceptions;
67
using JsonApiDotNetCore.Models;
78
using JsonApiDotNetCore.Services;
89
using JsonApiDotNetCoreExample.Models;
910
using Microsoft.AspNetCore.Mvc;
10-
using Microsoft.Extensions.Logging;
1111

1212
namespace JsonApiDotNetCoreExample.Controllers
1313
{
@@ -17,9 +17,8 @@ public class TodoItemsCustomController : CustomJsonApiController<TodoItem>
1717
{
1818
public TodoItemsCustomController(
1919
IJsonApiOptions options,
20-
IResourceService<TodoItem> resourceService,
21-
ILoggerFactory loggerFactory)
22-
: base(options, resourceService, loggerFactory)
20+
IResourceService<TodoItem> resourceService)
21+
: base(options, resourceService)
2322
{ }
2423
}
2524

@@ -28,8 +27,7 @@ public class CustomJsonApiController<T>
2827
{
2928
public CustomJsonApiController(
3029
IJsonApiOptions options,
31-
IResourceService<T, int> resourceService,
32-
ILoggerFactory loggerFactory)
30+
IResourceService<T, int> resourceService)
3331
: base(options, resourceService)
3432
{
3533
}
@@ -70,22 +68,29 @@ public async Task<IActionResult> GetAsync()
7068
[HttpGet("{id}")]
7169
public async Task<IActionResult> GetAsync(TId id)
7270
{
73-
var entity = await _resourceService.GetAsync(id);
74-
75-
if (entity == null)
71+
try
72+
{
73+
var entity = await _resourceService.GetAsync(id);
74+
return Ok(entity);
75+
}
76+
catch (ResourceNotFoundException)
77+
{
7678
return NotFound();
77-
78-
return Ok(entity);
79+
}
7980
}
8081

8182
[HttpGet("{id}/relationships/{relationshipName}")]
8283
public async Task<IActionResult> GetRelationshipsAsync(TId id, string relationshipName)
8384
{
84-
var relationship = _resourceService.GetRelationshipAsync(id, relationshipName);
85-
if (relationship == null)
85+
try
86+
{
87+
var relationship = await _resourceService.GetRelationshipsAsync(id, relationshipName);
88+
return Ok(relationship);
89+
}
90+
catch (ResourceNotFoundException)
91+
{
8692
return NotFound();
87-
88-
return await GetRelationshipAsync(id, relationshipName);
93+
}
8994
}
9095

9196
[HttpGet("{id}/{relationshipName}")]
@@ -115,12 +120,15 @@ public async Task<IActionResult> PatchAsync(TId id, [FromBody] T entity)
115120
if (entity == null)
116121
return UnprocessableEntity();
117122

118-
var updatedEntity = await _resourceService.UpdateAsync(id, entity);
119-
120-
if (updatedEntity == null)
123+
try
124+
{
125+
var updatedEntity = await _resourceService.UpdateAsync(id, entity);
126+
return Ok(updatedEntity);
127+
}
128+
catch (ResourceNotFoundException)
129+
{
121130
return NotFound();
122-
123-
return Ok(updatedEntity);
131+
}
124132
}
125133

126134
[HttpPatch("{id}/relationships/{relationshipName}")]
@@ -133,11 +141,7 @@ public async Task<IActionResult> PatchRelationshipsAsync(TId id, string relation
133141
[HttpDelete("{id}")]
134142
public async Task<IActionResult> DeleteAsync(TId id)
135143
{
136-
var wasDeleted = await _resourceService.DeleteAsync(id);
137-
138-
if (!wasDeleted)
139-
return NotFound();
140-
144+
await _resourceService.DeleteAsync(id);
141145
return NoContent();
142146
}
143147
}

src/Examples/NoEntityFrameworkExample/Services/TodoItemService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public async Task<TodoItem> CreateAsync(TodoItem entity)
6363
})).SingleOrDefault();
6464
}
6565

66-
public Task<bool> DeleteAsync(int id)
66+
public Task DeleteAsync(int id)
6767
{
6868
throw new NotImplementedException();
6969
}

src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,6 @@ public virtual async Task<IActionResult> GetAsync(TId id)
7878

7979
if (_getById == null) throw new RequestMethodNotAllowedException(HttpMethod.Get);
8080
var entity = await _getById.GetAsync(id);
81-
if (entity == null)
82-
{
83-
return NotFound();
84-
}
85-
8681
return Ok(entity);
8782
}
8883

@@ -92,10 +87,6 @@ public virtual async Task<IActionResult> GetRelationshipsAsync(TId id, string re
9287

9388
if (_getRelationships == null) throw new RequestMethodNotAllowedException(HttpMethod.Get);
9489
var relationship = await _getRelationships.GetRelationshipsAsync(id, relationshipName);
95-
if (relationship == null)
96-
{
97-
return NotFound();
98-
}
9990

10091
return Ok(relationship);
10192
}
@@ -117,17 +108,17 @@ public virtual async Task<IActionResult> PostAsync([FromBody] T entity)
117108
throw new RequestMethodNotAllowedException(HttpMethod.Post);
118109

119110
if (entity == null)
120-
return UnprocessableEntity();
111+
throw new InvalidRequestBodyException(null, null, null);
121112

122113
if (!_jsonApiOptions.AllowClientGeneratedIds && !string.IsNullOrEmpty(entity.StringId))
123-
return Forbidden();
114+
throw new ResourceIdInPostRequestNotAllowedException();
124115

125116
if (_jsonApiOptions.ValidateModelState && !ModelState.IsValid)
126117
throw new InvalidModelStateException(ModelState, typeof(T), _jsonApiOptions);
127118

128119
entity = await _create.CreateAsync(entity);
129120

130-
return Created($"{HttpContext.Request.Path}/{entity.Id}", entity);
121+
return Created($"{HttpContext.Request.Path}/{entity.StringId}", entity);
131122
}
132123

133124
public virtual async Task<IActionResult> PatchAsync(TId id, [FromBody] T entity)
@@ -136,19 +127,12 @@ public virtual async Task<IActionResult> PatchAsync(TId id, [FromBody] T entity)
136127

137128
if (_update == null) throw new RequestMethodNotAllowedException(HttpMethod.Patch);
138129
if (entity == null)
139-
return UnprocessableEntity();
130+
throw new InvalidRequestBodyException(null, null, null);
140131

141132
if (_jsonApiOptions.ValidateModelState && !ModelState.IsValid)
142133
throw new InvalidModelStateException(ModelState, typeof(T), _jsonApiOptions);
143134

144135
var updatedEntity = await _update.UpdateAsync(id, entity);
145-
146-
if (updatedEntity == null)
147-
{
148-
return NotFound();
149-
}
150-
151-
152136
return Ok(updatedEntity);
153137
}
154138

@@ -166,9 +150,8 @@ public virtual async Task<IActionResult> DeleteAsync(TId id)
166150
_logger.LogTrace($"Entering {nameof(DeleteAsync)}('{id}).");
167151

168152
if (_delete == null) throw new RequestMethodNotAllowedException(HttpMethod.Delete);
169-
var wasDeleted = await _delete.DeleteAsync(id);
170-
if (!wasDeleted)
171-
return NotFound();
153+
await _delete.DeleteAsync(id);
154+
172155
return NoContent();
173156
}
174157
}

src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System.Collections.Generic;
22
using System.Linq;
3-
using System.Net;
43
using JsonApiDotNetCore.Middleware;
54
using JsonApiDotNetCore.Models.JsonApiDocuments;
65
using Microsoft.AspNetCore.Mvc;
@@ -10,17 +9,12 @@ namespace JsonApiDotNetCore.Controllers
109
[ServiceFilter(typeof(IQueryParameterActionFilter))]
1110
public abstract class JsonApiControllerMixin : ControllerBase
1211
{
13-
protected IActionResult Forbidden()
14-
{
15-
return new StatusCodeResult((int)HttpStatusCode.Forbidden);
16-
}
17-
1812
protected IActionResult Error(Error error)
1913
{
20-
return Errors(new[] {error});
14+
return Error(new[] {error});
2115
}
2216

23-
protected IActionResult Errors(IEnumerable<Error> errors)
17+
protected IActionResult Error(IEnumerable<Error> errors)
2418
{
2519
var document = new ErrorDocument(errors.ToList());
2620

src/JsonApiDotNetCore/Exceptions/JsonApiException.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System;
2-
using System.Net;
32
using JsonApiDotNetCore.Models.JsonApiDocuments;
43
using Newtonsoft.Json;
54

@@ -26,15 +25,6 @@ public JsonApiException(Error error, Exception innerException)
2625
Error = error;
2726
}
2827

29-
public JsonApiException(HttpStatusCode status, string message)
30-
: base(message)
31-
{
32-
Error = new Error(status)
33-
{
34-
Title = message
35-
};
36-
}
37-
3828
public override string Message => "Error = " + JsonConvert.SerializeObject(Error, _errorSerializerSettings);
3929
}
4030
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Net;
2+
using JsonApiDotNetCore.Models.JsonApiDocuments;
3+
4+
namespace JsonApiDotNetCore.Exceptions
5+
{
6+
/// <summary>
7+
/// The error that is thrown when a relationship does not exist.
8+
/// </summary>
9+
public sealed class RelationshipNotFoundException : JsonApiException
10+
{
11+
public RelationshipNotFoundException(string relationshipName, string containingResourceName) : base(new Error(HttpStatusCode.NotFound)
12+
{
13+
Title = "The requested relationship does not exist.",
14+
Detail = $"The resource '{containingResourceName}' does not contain a relationship named '{relationshipName}'."
15+
})
16+
{
17+
}
18+
}
19+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System.Net;
2+
using JsonApiDotNetCore.Models.JsonApiDocuments;
3+
4+
namespace JsonApiDotNetCore.Exceptions
5+
{
6+
/// <summary>
7+
/// The error that is thrown when a POST request is received that contains a client-generated ID.
8+
/// </summary>
9+
public sealed class ResourceIdInPostRequestNotAllowedException : JsonApiException
10+
{
11+
public ResourceIdInPostRequestNotAllowedException()
12+
: base(new Error(HttpStatusCode.Forbidden)
13+
{
14+
Title = "Specifying the resource id in POST requests is not allowed.",
15+
Source =
16+
{
17+
Pointer = "/data/id"
18+
}
19+
})
20+
{
21+
}
22+
}
23+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Net;
2+
using JsonApiDotNetCore.Models.JsonApiDocuments;
3+
4+
namespace JsonApiDotNetCore.Exceptions
5+
{
6+
/// <summary>
7+
/// The error that is thrown when a resource does not exist.
8+
/// </summary>
9+
public sealed class ResourceNotFoundException : JsonApiException
10+
{
11+
public ResourceNotFoundException(string resourceId, string resourceType) : base(new Error(HttpStatusCode.NotFound)
12+
{
13+
Title = "The requested resource does not exist.",
14+
Detail = $"Resource of type '{resourceType}' with id '{resourceId}' does not exist."
15+
})
16+
{
17+
}
18+
}
19+
}

src/JsonApiDotNetCore/Extensions/TypeExtensions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Linq;
66
using System.Net;
77
using JsonApiDotNetCore.Exceptions;
8+
using JsonApiDotNetCore.Models;
89

910
namespace JsonApiDotNetCore.Extensions
1011
{
@@ -83,6 +84,13 @@ public static IEnumerable GetEmptyCollection(this Type t)
8384
return list;
8485
}
8586

87+
public static string GetResourceStringId<TResource, TId>(TId id) where TResource : class, IIdentifiable<TId>
88+
{
89+
var tempResource = typeof(TResource).New<TResource>();
90+
tempResource.Id = id;
91+
return tempResource.StringId;
92+
}
93+
8694
public static object New(this Type t)
8795
{
8896
return New<object>(t);

src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ private ResourceContext GetCurrentEntity()
233233
}
234234
if (_routeValues.TryGetValue("relationshipName", out object relationshipName))
235235
{
236-
_currentRequest.RequestRelationship = requestResource.Relationships.Single(r => r.PublicRelationshipName == (string)relationshipName);
236+
_currentRequest.RequestRelationship = requestResource.Relationships.SingleOrDefault(r => r.PublicRelationshipName == (string)relationshipName);
237237
}
238238
return requestResource;
239239
}

0 commit comments

Comments
 (0)