Skip to content

Commit 956f473

Browse files
committed
support get relationship requests in operations
1 parent 2981caf commit 956f473

File tree

7 files changed

+111
-15
lines changed

7 files changed

+111
-15
lines changed

src/JsonApiDotNetCore/Builders/DocumentBuilderOptions.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Text;
4-
51
namespace JsonApiDotNetCore.Builders
62
{
7-
public struct DocumentBuilderOptions
3+
public struct DocumentBuilderOptions
84
{
95
public DocumentBuilderOptions(bool omitNullValuedAttributes = false)
106
{

src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public class JsonApiOptions
5252
/// <code>options.AllowClientGeneratedIds = true;</code>
5353
/// </example>
5454
public bool AllowClientGeneratedIds { get; set; }
55-
55+
5656
/// <summary>
5757
/// The graph of all resources exposed by this application.
5858
/// </summary>
@@ -107,10 +107,11 @@ public class JsonApiOptions
107107
/// <summary>
108108
/// Whether or not to allow json:api v1.1 operation requests.
109109
/// This is a beta feature and there may be breaking changes
110-
/// in subsequent releases.
110+
/// in subsequent releases. For now, it should be considered
111+
/// experimental.
111112
/// </summary>
112113
/// <remarks>
113-
/// This will be enabled by default in JsonApiDotNetCore v2.2.1
114+
/// This will be enabled by default in a subsequent patch JsonApiDotNetCore v2.2.x
114115
/// </remarks>
115116
public bool EnableOperations { get; set; }
116117

@@ -144,7 +145,7 @@ public void BuildContextGraph(Action<IContextGraphBuilder> builder)
144145
ContextGraph = ContextGraphBuilder.Build();
145146
}
146147

147-
public void EnableExtension(JsonApiExtension extension)
148+
public void EnableExtension(JsonApiExtension extension)
148149
=> EnabledExtensions.Add(extension);
149150

150151
internal IContextGraphBuilder ContextGraphBuilder { get; } = new ContextGraphBuilder();

src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ public static void AddJsonApiInternals(
109109
services.AddScoped(typeof(IGetByIdService<>), typeof(EntityResourceService<>));
110110
services.AddScoped(typeof(IGetByIdService<,>), typeof(EntityResourceService<,>));
111111

112+
services.AddScoped(typeof(IGetRelationshipService<,>), typeof(EntityResourceService<>));
113+
services.AddScoped(typeof(IGetRelationshipService<,>), typeof(EntityResourceService<,>));
114+
112115
services.AddScoped(typeof(IUpdateService<>), typeof(EntityResourceService<>));
113116
services.AddScoped(typeof(IUpdateService<,>), typeof(EntityResourceService<,>));
114117

src/JsonApiDotNetCore/Services/EntityResourceService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ public virtual async Task<object> GetRelationshipAsync(TId id, string relationsh
9696
_logger.LogTrace($"Looking up '{relationshipName}'...");
9797

9898
var entity = await _entities.GetAndIncludeAsync(id, relationshipName);
99+
// TODO: it would be better if we could distinguish whether or not the relationship was not found,
100+
// vs the relationship not being set on the instance of T
99101
if (entity == null)
100102
throw new JsonApiException(404, $"Relationship {relationshipName} not found.");
101103

src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using System;
12
using System.Collections.Generic;
3+
using System.Linq;
24
using System.Threading.Tasks;
35
using JsonApiDotNetCore.Builders;
46
using JsonApiDotNetCore.Internal;
@@ -8,64 +10,84 @@
810

911
namespace JsonApiDotNetCore.Services.Operations.Processors
1012
{
13+
/// <summary>
14+
/// Handles all "<see cref="OperationCode.get"/>" operations
15+
/// </summary>
16+
/// <typeparam name="T">The resource type</typeparam>
1117
public interface IGetOpProcessor<T> : IOpProcessor
1218
where T : class, IIdentifiable<int>
1319
{ }
1420

21+
/// <summary>
22+
/// Handles all "<see cref="OperationCode.get"/>" operations
23+
/// </summary>
24+
/// <typeparam name="T">The resource type</typeparam>
25+
/// <typeparam name="TId">The resource identifier type</typeparam>
1526
public interface IGetOpProcessor<T, TId> : IOpProcessor
1627
where T : class, IIdentifiable<TId>
1728
{ }
1829

30+
/// <inheritdoc />
1931
public class GetOpProcessor<T> : GetOpProcessor<T, int>
2032
where T : class, IIdentifiable<int>
2133
{
34+
/// <inheritdoc />
2235
public GetOpProcessor(
2336
IGetAllService<T, int> getAll,
2437
IGetByIdService<T, int> getById,
38+
IGetRelationshipService<T, int> getRelationship,
2539
IJsonApiDeSerializer deSerializer,
2640
IDocumentBuilder documentBuilder,
2741
IContextGraph contextGraph,
2842
IJsonApiContext jsonApiContext
29-
) : base(getAll, getById, deSerializer, documentBuilder, contextGraph, jsonApiContext)
43+
) : base(getAll, getById, getRelationship, deSerializer, documentBuilder, contextGraph, jsonApiContext)
3044
{ }
3145
}
3246

47+
/// <inheritdoc />
3348
public class GetOpProcessor<T, TId> : IGetOpProcessor<T, TId>
3449
where T : class, IIdentifiable<TId>
3550
{
3651
private readonly IGetAllService<T, TId> _getAll;
3752
private readonly IGetByIdService<T, TId> _getById;
53+
private readonly IGetRelationshipService<T, TId> _getRelationship;
3854
private readonly IJsonApiDeSerializer _deSerializer;
3955
private readonly IDocumentBuilder _documentBuilder;
4056
private readonly IContextGraph _contextGraph;
4157
private readonly IJsonApiContext _jsonApiContext;
4258

59+
/// <inheritdoc />
4360
public GetOpProcessor(
4461
IGetAllService<T, TId> getAll,
4562
IGetByIdService<T, TId> getById,
63+
IGetRelationshipService<T, TId> getRelationship,
4664
IJsonApiDeSerializer deSerializer,
4765
IDocumentBuilder documentBuilder,
4866
IContextGraph contextGraph,
4967
IJsonApiContext jsonApiContext)
5068
{
5169
_getAll = getAll;
5270
_getById = getById;
71+
_getRelationship = getRelationship;
5372
_deSerializer = deSerializer;
5473
_documentBuilder = documentBuilder;
5574
_contextGraph = contextGraph;
5675
_jsonApiContext = jsonApiContext.ApplyContext<T>(this);
5776
}
5877

78+
/// <inheritdoc />
5979
public async Task<Operation> ProcessAsync(Operation operation)
6080
{
6181
var operationResult = new Operation
6282
{
6383
Op = OperationCode.get
6484
};
6585

66-
operationResult.Data = string.IsNullOrWhiteSpace(operation.Ref.Id?.ToString())
86+
operationResult.Data = string.IsNullOrWhiteSpace(operation.Ref.Id)
6787
? await GetAllAsync(operation)
68-
: await GetByIdAsync(operation);
88+
: string.IsNullOrWhiteSpace(operation.Ref.Relationship)
89+
? await GetByIdAsync(operation)
90+
: await GetRelationshipAsync(operation);
6991

7092
return operationResult;
7193
}
@@ -88,7 +110,7 @@ private async Task<object> GetAllAsync(Operation operation)
88110

89111
private async Task<object> GetByIdAsync(Operation operation)
90112
{
91-
var id = TypeHelper.ConvertType<TId>(operation.Ref.Id);
113+
var id = GetReferenceId(operation);
92114
var result = await _getById.GetAsync(id);
93115

94116
// this is a bit ugly but we need to bomb the entire transaction if the entity cannot be found
@@ -104,5 +126,23 @@ private async Task<object> GetByIdAsync(Operation operation)
104126

105127
return doc;
106128
}
129+
130+
private async Task<object> GetRelationshipAsync(Operation operation)
131+
{
132+
var id = GetReferenceId(operation);
133+
var result = await _getRelationship.GetRelationshipAsync(id, operation.Ref.Relationship);
134+
135+
// TODO: need a better way to get the ContextEntity from a relationship name
136+
// when no generic parameter is available
137+
var relationshipType = _contextGraph.GetContextEntity(operation.GetResourceTypeName())
138+
.Relationships.Single(r => r.Is(operation.Ref.Relationship)).Type;
139+
var relatedContextEntity = _jsonApiContext.ContextGraph.GetContextEntity(relationshipType);
140+
141+
var doc = _documentBuilder.GetData(relatedContextEntity, result as IIdentifiable); // TODO: if this is safe, then it should be cast in the GetRelationshipAsync call
142+
143+
return doc;
144+
}
145+
146+
private TId GetReferenceId(Operation operation) => TypeHelper.ConvertType<TId>(operation.Ref.Id);
107147
}
108148
}

src/JsonApiDotNetCore/api/.manifest

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@
381381
"JsonApiDotNetCore.Models.AttrAttribute.#ctor(System.String,System.Boolean,System.Boolean,System.Boolean)": "JsonApiDotNetCore.Models.AttrAttribute.yml",
382382
"JsonApiDotNetCore.Models.AttrAttribute.GetValue(System.Object)": "JsonApiDotNetCore.Models.AttrAttribute.yml",
383383
"JsonApiDotNetCore.Models.AttrAttribute.InternalAttributeName": "JsonApiDotNetCore.Models.AttrAttribute.yml",
384+
"JsonApiDotNetCore.Models.AttrAttribute.Is(System.String)": "JsonApiDotNetCore.Models.AttrAttribute.yml",
384385
"JsonApiDotNetCore.Models.AttrAttribute.IsFilterable": "JsonApiDotNetCore.Models.AttrAttribute.yml",
385386
"JsonApiDotNetCore.Models.AttrAttribute.IsImmutable": "JsonApiDotNetCore.Models.AttrAttribute.yml",
386387
"JsonApiDotNetCore.Models.AttrAttribute.IsSortable": "JsonApiDotNetCore.Models.AttrAttribute.yml",
@@ -459,6 +460,7 @@
459460
"JsonApiDotNetCore.Models.RelationshipAttribute.DocumentLinks": "JsonApiDotNetCore.Models.RelationshipAttribute.yml",
460461
"JsonApiDotNetCore.Models.RelationshipAttribute.Equals(System.Object)": "JsonApiDotNetCore.Models.RelationshipAttribute.yml",
461462
"JsonApiDotNetCore.Models.RelationshipAttribute.InternalRelationshipName": "JsonApiDotNetCore.Models.RelationshipAttribute.yml",
463+
"JsonApiDotNetCore.Models.RelationshipAttribute.Is(System.String)": "JsonApiDotNetCore.Models.RelationshipAttribute.yml",
462464
"JsonApiDotNetCore.Models.RelationshipAttribute.IsHasMany": "JsonApiDotNetCore.Models.RelationshipAttribute.yml",
463465
"JsonApiDotNetCore.Models.RelationshipAttribute.IsHasOne": "JsonApiDotNetCore.Models.RelationshipAttribute.yml",
464466
"JsonApiDotNetCore.Models.RelationshipAttribute.PublicRelationshipName": "JsonApiDotNetCore.Models.RelationshipAttribute.yml",
@@ -638,9 +640,9 @@
638640
"JsonApiDotNetCore.Services.Operations.Processors.CreateOpProcessor`2.#ctor(JsonApiDotNetCore.Services.ICreateService{`0,`1},JsonApiDotNetCore.Serialization.IJsonApiDeSerializer,JsonApiDotNetCore.Builders.IDocumentBuilder,JsonApiDotNetCore.Internal.IContextGraph)": "JsonApiDotNetCore.Services.Operations.Processors.CreateOpProcessor-2.yml",
639641
"JsonApiDotNetCore.Services.Operations.Processors.CreateOpProcessor`2.ProcessAsync(JsonApiDotNetCore.Models.Operations.Operation)": "JsonApiDotNetCore.Services.Operations.Processors.CreateOpProcessor-2.yml",
640642
"JsonApiDotNetCore.Services.Operations.Processors.GetOpProcessor`1": "JsonApiDotNetCore.Services.Operations.Processors.GetOpProcessor-1.yml",
641-
"JsonApiDotNetCore.Services.Operations.Processors.GetOpProcessor`1.#ctor(JsonApiDotNetCore.Services.IGetAllService{`0,System.Int32},JsonApiDotNetCore.Services.IGetByIdService{`0,System.Int32},JsonApiDotNetCore.Serialization.IJsonApiDeSerializer,JsonApiDotNetCore.Builders.IDocumentBuilder,JsonApiDotNetCore.Internal.IContextGraph,JsonApiDotNetCore.Services.IJsonApiContext)": "JsonApiDotNetCore.Services.Operations.Processors.GetOpProcessor-1.yml",
643+
"JsonApiDotNetCore.Services.Operations.Processors.GetOpProcessor`1.#ctor(JsonApiDotNetCore.Services.IGetAllService{`0,System.Int32},JsonApiDotNetCore.Services.IGetByIdService{`0,System.Int32},JsonApiDotNetCore.Services.IGetRelationshipService{`0,System.Int32},JsonApiDotNetCore.Serialization.IJsonApiDeSerializer,JsonApiDotNetCore.Builders.IDocumentBuilder,JsonApiDotNetCore.Internal.IContextGraph,JsonApiDotNetCore.Services.IJsonApiContext)": "JsonApiDotNetCore.Services.Operations.Processors.GetOpProcessor-1.yml",
642644
"JsonApiDotNetCore.Services.Operations.Processors.GetOpProcessor`2": "JsonApiDotNetCore.Services.Operations.Processors.GetOpProcessor-2.yml",
643-
"JsonApiDotNetCore.Services.Operations.Processors.GetOpProcessor`2.#ctor(JsonApiDotNetCore.Services.IGetAllService{`0,`1},JsonApiDotNetCore.Services.IGetByIdService{`0,`1},JsonApiDotNetCore.Serialization.IJsonApiDeSerializer,JsonApiDotNetCore.Builders.IDocumentBuilder,JsonApiDotNetCore.Internal.IContextGraph,JsonApiDotNetCore.Services.IJsonApiContext)": "JsonApiDotNetCore.Services.Operations.Processors.GetOpProcessor-2.yml",
645+
"JsonApiDotNetCore.Services.Operations.Processors.GetOpProcessor`2.#ctor(JsonApiDotNetCore.Services.IGetAllService{`0,`1},JsonApiDotNetCore.Services.IGetByIdService{`0,`1},JsonApiDotNetCore.Services.IGetRelationshipService{`0,`1},JsonApiDotNetCore.Serialization.IJsonApiDeSerializer,JsonApiDotNetCore.Builders.IDocumentBuilder,JsonApiDotNetCore.Internal.IContextGraph,JsonApiDotNetCore.Services.IJsonApiContext)": "JsonApiDotNetCore.Services.Operations.Processors.GetOpProcessor-2.yml",
644646
"JsonApiDotNetCore.Services.Operations.Processors.GetOpProcessor`2.ProcessAsync(JsonApiDotNetCore.Models.Operations.Operation)": "JsonApiDotNetCore.Services.Operations.Processors.GetOpProcessor-2.yml",
645647
"JsonApiDotNetCore.Services.Operations.Processors.ICreateOpProcessor`1": "JsonApiDotNetCore.Services.Operations.Processors.ICreateOpProcessor-1.yml",
646648
"JsonApiDotNetCore.Services.Operations.Processors.ICreateOpProcessor`2": "JsonApiDotNetCore.Services.Operations.Processors.ICreateOpProcessor-2.yml",
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net;
5+
using System.Threading.Tasks;
6+
using Bogus;
7+
using JsonApiDotNetCore.Models.Operations;
8+
using JsonApiDotNetCoreExample.Data;
9+
using OperationsExampleTests.Factories;
10+
using Xunit;
11+
12+
namespace OperationsExampleTests
13+
{
14+
public class GetRelationshipTests : Fixture, IDisposable
15+
{
16+
private readonly Faker _faker = new Faker();
17+
18+
[Fact]
19+
public async Task Can_Get_Article_Author()
20+
{
21+
// arrange
22+
var context = GetService<AppDbContext>();
23+
var author = AuthorFactory.Get();
24+
var article = ArticleFactory.Get();
25+
article.Author = author;
26+
context.Articles.Add(article);
27+
context.SaveChanges();
28+
29+
var content = new
30+
{
31+
operations = new[] {
32+
new Dictionary<string, object> {
33+
{ "op", "get"},
34+
{ "ref", new { type = "articles", id = article.StringId, relationship = nameof(article.Author) } }
35+
}
36+
}
37+
};
38+
39+
// act
40+
var (response, data) = await PatchAsync<OperationsDocument>("api/bulk", content);
41+
42+
// assert
43+
Assert.NotNull(response);
44+
Assert.NotNull(data);
45+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
46+
Assert.Equal(1, data.Operations.Count);
47+
var resourceObject = data.Operations.Single().DataObject;
48+
Assert.Equal(author.Id.ToString(), resourceObject.Id);
49+
Assert.Equal("authors", resourceObject.Type);
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)