Skip to content

Commit a683713

Browse files
authored
Merge pull request #43 from Research-Institute/missing-spec-tests
Missing spec tests
2 parents dbc1ba8 + 6b3aca6 commit a683713

22 files changed

+615
-13
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,24 @@ public class Person : Identifiable<Guid>
7979
{ }
8080
```
8181

82+
You can use the non-generic `Identifiable` if your primary key is an integer:
83+
84+
```csharp
85+
public class Person : Identifiable
86+
{ }
87+
```
88+
89+
If you need to hang annotations or attributes on the `Id` property, you can override the virtual member:
90+
91+
```csharp
92+
public class Person : Identifiable
93+
{
94+
[Key]
95+
[Column("person_id")]
96+
public override int Id { get; set; }
97+
}
98+
```
99+
82100
#### Specifying Public Attributes
83101

84102
If you want an attribute on your model to be publicly available,

src/JsonApiDotNetCore/Controllers/JsonApiController.cs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Collections;
21
using System.Collections.Generic;
32
using System.Linq;
43
using System.Threading.Tasks;
@@ -139,9 +138,13 @@ public virtual async Task<IActionResult> PostAsync([FromBody] T entity)
139138
return UnprocessableEntity();
140139
}
141140

141+
var stringId = entity.Id.ToString();
142+
if(stringId.Length > 0 && stringId != "0")
143+
return Forbidden();
144+
142145
await _entities.CreateAsync(entity);
143146

144-
return Created(HttpContext.Request.Path, entity);
147+
return Created($"{HttpContext.Request.Path}/{entity.Id}", entity);
145148
}
146149

147150
[HttpPatch("{id}")]
@@ -155,9 +158,41 @@ public virtual async Task<IActionResult> PatchAsync(TId id, [FromBody] T entity)
155158

156159
var updatedEntity = await _entities.UpdateAsync(id, entity);
157160

161+
if(updatedEntity == null) return NotFound();
162+
158163
return Ok(updatedEntity);
159164
}
160165

166+
[HttpPatch("{id}/relationships/{relationshipName}")]
167+
public virtual async Task<IActionResult> PatchRelationshipsAsync(TId id, string relationshipName, [FromBody] List<DocumentData> relationships)
168+
{
169+
relationshipName = _jsonApiContext.ContextGraph
170+
.GetRelationshipName<T>(relationshipName.ToProperCase());
171+
172+
if (relationshipName == null)
173+
{
174+
_logger?.LogInformation($"Relationship name not specified returning 422");
175+
return UnprocessableEntity();
176+
}
177+
178+
var entity = await _entities.GetAndIncludeAsync(id, relationshipName);
179+
180+
if (entity == null)
181+
return NotFound();
182+
183+
var relationship = _jsonApiContext.ContextGraph
184+
.GetContextEntity(typeof(T))
185+
.Relationships
186+
.FirstOrDefault(r => r.RelationshipName == relationshipName);
187+
188+
var relationshipIds = relationships.Select(r=>r.Id);
189+
190+
await _entities.UpdateRelationshipsAsync(entity, relationship, relationshipIds);
191+
192+
return Ok();
193+
194+
}
195+
161196
[HttpDelete("{id}")]
162197
public virtual async Task<IActionResult> DeleteAsync(TId id)
163198
{

src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,10 @@ protected IActionResult UnprocessableEntity()
1111
{
1212
return new StatusCodeResult(422);
1313
}
14+
15+
protected IActionResult Forbidden()
16+
{
17+
return new StatusCodeResult(403);
18+
}
1419
}
1520
}

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,13 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
105105

106106
await _context.SaveChangesAsync();
107107

108-
return oldEntity;
108+
return oldEntity;
109+
}
110+
111+
public async Task UpdateRelationshipsAsync(object parent, Relationship relationship, IEnumerable<string> relationshipIds)
112+
{
113+
var genericProcessor = GenericProcessorFactory.GetProcessor(relationship.BaseType, _context);
114+
await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds);
109115
}
110116

111117
public virtual async Task<bool> DeleteAsync(TId id)

src/JsonApiDotNetCore/Data/IEntityRepository.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Collections.Generic;
22
using System.Linq;
33
using System.Threading.Tasks;
4+
using JsonApiDotNetCore.Internal;
45
using JsonApiDotNetCore.Internal.Query;
56
using JsonApiDotNetCore.Models;
67

@@ -33,6 +34,8 @@ public interface IEntityRepository<TEntity, in TId>
3334

3435
Task<TEntity> UpdateAsync(TId id, TEntity entity);
3536

37+
Task UpdateRelationshipsAsync(object parent, Relationship relationship, IEnumerable<string> relationshipIds);
38+
3639
Task<bool> DeleteAsync(TId id);
3740
}
3841
}

src/JsonApiDotNetCore/Formatters/JsonApiInputFormatter.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ public Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
4141
{
4242
var body = GetRequestBody(context.HttpContext.Request.Body);
4343
var jsonApiContext = GetService<IJsonApiContext>(context);
44-
var model = JsonApiDeSerializer.Deserialize(body, jsonApiContext);
44+
var model = jsonApiContext.IsRelationshipPath ?
45+
JsonApiDeSerializer.DeserializeRelationship(body, jsonApiContext) :
46+
JsonApiDeSerializer.Deserialize(body, jsonApiContext);
4547

4648
if(model == null)
4749
logger?.LogError("An error occurred while de-serializing the payload");
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System.Collections;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
5+
using System.Threading.Tasks;
6+
using JsonApiDotNetCore.Extensions;
7+
using JsonApiDotNetCore.Models;
8+
using Microsoft.EntityFrameworkCore;
9+
10+
namespace JsonApiDotNetCore.Internal
11+
{
12+
public class GenericProcessor<T> : IGenericProcessor where T : class, IIdentifiable
13+
{
14+
private readonly DbContext _context;
15+
public GenericProcessor(DbContext context)
16+
{
17+
_context = context;
18+
}
19+
20+
public async Task UpdateRelationshipsAsync(object parent, Relationship relationship, IEnumerable<string> relationshipIds)
21+
{
22+
var relationshipType = relationship.BaseType;
23+
24+
// TODO: replace with relationship.IsMany
25+
if(relationship.Type.GetInterfaces().Contains(typeof(IEnumerable)))
26+
{
27+
var entities = _context.GetDbSet<T>().Where(x => relationshipIds.Contains(x.Id.ToString())).ToList();
28+
relationship.SetValue(parent, entities);
29+
}
30+
else
31+
{
32+
var entity = _context.GetDbSet<T>().SingleOrDefault(x => relationshipIds.First() == x.Id.ToString());
33+
relationship.SetValue(parent, entity);
34+
}
35+
36+
await _context.SaveChangesAsync();
37+
}
38+
}
39+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System;
2+
using Microsoft.EntityFrameworkCore;
3+
4+
namespace JsonApiDotNetCore.Internal
5+
{
6+
/// <summary>
7+
/// Used to generate a generic operations processor when the types
8+
/// are not know until runtime. The typical use case would be for
9+
/// accessing relationship data.
10+
/// </summary>
11+
public static class GenericProcessorFactory
12+
{
13+
public static IGenericProcessor GetProcessor(Type type, DbContext dbContext)
14+
{
15+
var repositoryType = typeof(GenericProcessor<>).MakeGenericType(type);
16+
return (IGenericProcessor)Activator.CreateInstance(repositoryType, dbContext);
17+
}
18+
}
19+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System.Collections.Generic;
2+
using System.Threading.Tasks;
3+
4+
namespace JsonApiDotNetCore.Internal
5+
{
6+
public interface IGenericProcessor
7+
{
8+
Task UpdateRelationshipsAsync(object parent, Relationship relationship, IEnumerable<string> relationshipIds);
9+
}
10+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System;
2+
using System.Linq;
3+
4+
namespace JsonApiDotNetCore.Internal
5+
{
6+
public static class JsonApiExceptionFactory
7+
{
8+
public static JsonApiException GetException(Exception exception)
9+
{
10+
var exceptionType = exception.GetType().ToString().Split('.').Last();
11+
switch(exceptionType)
12+
{
13+
case "JsonApiException":
14+
return (JsonApiException)exception;
15+
case "InvalidCastException":
16+
return new JsonApiException("409", exception.Message);
17+
default:
18+
return new JsonApiException("500", exception.Message);
19+
}
20+
}
21+
}
22+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,30 @@
11
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Reflection;
6+
using JsonApiDotNetCore.Extensions;
27

38
namespace JsonApiDotNetCore.Internal
49
{
510
public class Relationship
611
{
712
public Type Type { get; set; }
13+
public Type BaseType { get {
14+
return (Type.GetInterfaces().Contains(typeof(IEnumerable))) ?
15+
Type.GenericTypeArguments[0] :
16+
Type;
17+
} }
18+
819
public string RelationshipName { get; set; }
20+
21+
public void SetValue(object entity, object newValue)
22+
{
23+
var propertyInfo = entity
24+
.GetType()
25+
.GetProperty(RelationshipName);
26+
27+
propertyInfo.SetValue(entity, newValue);
28+
}
929
}
1030
}

src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,8 @@ public void OnException(ExceptionContext context)
1919
{
2020
_logger?.LogError(new EventId(), context.Exception, "An unhandled exception occurred during the request");
2121

22-
var jsonApiException = context.Exception as JsonApiException;
22+
var jsonApiException = JsonApiExceptionFactory.GetException(context.Exception);
2323

24-
if(jsonApiException == null)
25-
jsonApiException = new JsonApiException("500", context.Exception.Message);
26-
2724
var error = jsonApiException.GetError();
2825
var result = new ObjectResult(error);
2926
result.StatusCode = Convert.ToInt16(error.Status);

src/JsonApiDotNetCore/Models/Identifiable.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ public class Identifiable : Identifiable<int>
55

66
public class Identifiable<T> : IIdentifiable<T>, IIdentifiable
77
{
8-
public T Id { get; set; }
8+
public virtual T Id { get; set; }
99

1010
object IIdentifiable.Id
1111
{

src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using JsonApiDotNetCore.Models;
88
using JsonApiDotNetCore.Services;
99
using Newtonsoft.Json;
10+
using Newtonsoft.Json.Linq;
1011

1112
namespace JsonApiDotNetCore.Serialization
1213
{
@@ -15,12 +16,21 @@ public static class JsonApiDeSerializer
1516
public static object Deserialize(string requestBody, IJsonApiContext context)
1617
{
1718
var document = JsonConvert.DeserializeObject<Document>(requestBody);
18-
1919
var entity = DataToObject(document.Data, context);
20-
2120
return entity;
2221
}
2322

23+
public static object DeserializeRelationship(string requestBody, IJsonApiContext context)
24+
{
25+
var data = JToken.Parse(requestBody)["data"];
26+
27+
if(data is JArray)
28+
return data.ToObject<List<DocumentData>>();
29+
30+
return new List<DocumentData> { data.ToObject<DocumentData>() };
31+
}
32+
33+
2434
public static List<TEntity> DeserializeList<TEntity>(string requestBody, IJsonApiContext context)
2535
{
2636
var documents = JsonConvert.DeserializeObject<Documents>(requestBody);

src/JsonApiDotNetCore/Services/IJsonApiContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ public interface IJsonApiContext
1515
QuerySet QuerySet { get; set; }
1616
bool IsRelationshipData { get; set; }
1717
List<string> IncludedRelationships { get; set; }
18+
bool IsRelationshipPath { get; }
1819
PageManager PageManager { get; set; }
20+
1921
}
2022
}

src/JsonApiDotNetCore/Services/JsonApiContext.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ public JsonApiContext(
2727
public string BasePath { get; set; }
2828
public QuerySet QuerySet { get; set; }
2929
public bool IsRelationshipData { get; set; }
30+
public bool IsRelationshipPath { get; private set; }
3031
public List<string> IncludedRelationships { get; set; }
3132
public PageManager PageManager { get; set; }
3233

3334
public IJsonApiContext ApplyContext<T>()
3435
{
3536
var context = _httpContextAccessor.HttpContext;
37+
var path = context.Request.Path.Value.Split('/');
3638

3739
RequestEntity = ContextGraph.GetContextEntity(typeof(T));
3840

@@ -45,7 +47,7 @@ public IJsonApiContext ApplyContext<T>()
4547
var linkBuilder = new LinkBuilder(this);
4648
BasePath = linkBuilder.GetBasePath(context, RequestEntity.EntityName);
4749
PageManager = GetPageManager();
48-
50+
IsRelationshipPath = path[path.Length - 2] == "relationships";
4951
return this;
5052
}
5153

0 commit comments

Comments
 (0)