Skip to content

Commit 69323c6

Browse files
committed
replace operations
1 parent 0f99794 commit 69323c6

File tree

7 files changed

+230
-9
lines changed

7 files changed

+230
-9
lines changed

src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs

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

113+
services.AddScoped(typeof(IUpdateService<>), typeof(EntityResourceService<>));
114+
services.AddScoped(typeof(IUpdateService<,>), typeof(EntityResourceService<,>));
115+
113116
services.AddScoped(typeof(IResourceService<>), typeof(EntityResourceService<>));
114117
services.AddScoped(typeof(IResourceService<,>), typeof(EntityResourceService<,>));
115118
services.AddSingleton<JsonApiOptions>(jsonApiOptions);
@@ -142,6 +145,9 @@ private static void AddOperationServices(IServiceCollection services)
142145
services.AddScoped(typeof(IGetOpProcessor<>), typeof(GetOpProcessor<>));
143146
services.AddScoped(typeof(IGetOpProcessor<,>), typeof(GetOpProcessor<,>));
144147

148+
services.AddScoped(typeof(IReplaceOpProcessor<>), typeof(ReplaceOpProcessor<>));
149+
services.AddScoped(typeof(IReplaceOpProcessor<,>), typeof(ReplaceOpProcessor<,>));
150+
145151
services.AddSingleton<IOperationProcessorResolver, OperationProcessorResolver>();
146152
services.AddSingleton<IGenericProcessorFactory, GenericProcessorFactory>();
147153
}

src/JsonApiDotNetCore/Services/Operations/OperationProcessorResolver.cs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ namespace JsonApiDotNetCore.Services.Operations
88
public interface IOperationProcessorResolver
99
{
1010
IOpProcessor LocateCreateService(Operation operation);
11-
IOpProcessor LocateGeteService(Operation operation);
11+
IOpProcessor LocateGetService(Operation operation);
12+
IOpProcessor LocateReplaceService(Operation operation);
1213
}
1314

1415
public class OperationProcessorResolver : IOperationProcessorResolver
@@ -17,6 +18,7 @@ public class OperationProcessorResolver : IOperationProcessorResolver
1718
private readonly IJsonApiContext _context;
1819
private ConcurrentDictionary<string, IOpProcessor> _createOpProcessors = new ConcurrentDictionary<string, IOpProcessor>();
1920
private ConcurrentDictionary<string, IOpProcessor> _getOpProcessors = new ConcurrentDictionary<string, IOpProcessor>();
21+
private ConcurrentDictionary<string, IOpProcessor> _replaceOpProcessors = new ConcurrentDictionary<string, IOpProcessor>();
2022

2123
public OperationProcessorResolver(
2224
IGenericProcessorFactory processorFactory,
@@ -45,7 +47,7 @@ public IOpProcessor LocateCreateService(Operation operation)
4547
return processor;
4648
}
4749

48-
public IOpProcessor LocateGeteService(Operation operation)
50+
public IOpProcessor LocateGetService(Operation operation)
4951
{
5052
var resource = operation.GetResourceTypeName();
5153

@@ -61,5 +63,22 @@ public IOpProcessor LocateGeteService(Operation operation)
6163

6264
return processor;
6365
}
66+
67+
public IOpProcessor LocateReplaceService(Operation operation)
68+
{
69+
var resource = operation.GetResourceTypeName();
70+
71+
if (_replaceOpProcessors.TryGetValue(resource, out IOpProcessor cachedProcessor))
72+
return cachedProcessor;
73+
74+
var contextEntity = _context.ContextGraph.GetContextEntity(resource);
75+
var processor = _processorFactory.GetProcessor<IOpProcessor>(
76+
typeof(IReplaceOpProcessor<,>), contextEntity.EntityType, contextEntity.IdentityType
77+
);
78+
79+
_replaceOpProcessors[resource] = processor;
80+
81+
return processor;
82+
}
6483
}
6584
}

src/JsonApiDotNetCore/Services/Operations/OperationsProcessor.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ private IOpProcessor GetOperationsProcessor(Operation op)
6868
case OperationCode.add:
6969
return _processorResolver.LocateCreateService(op);
7070
case OperationCode.get:
71-
return _processorResolver.LocateGeteService(op);
71+
return _processorResolver.LocateGetService(op);
72+
case OperationCode.replace:
73+
return _processorResolver.LocateReplaceService(op);
7274
default:
7375
throw new JsonApiException(400, $"'{op.Op}' is not a valid operation code");
7476
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using JsonApiDotNetCore.Builders;
4+
using JsonApiDotNetCore.Internal;
5+
using JsonApiDotNetCore.Models;
6+
using JsonApiDotNetCore.Models.Operations;
7+
using JsonApiDotNetCore.Serialization;
8+
using Newtonsoft.Json;
9+
10+
namespace JsonApiDotNetCore.Services.Operations.Processors
11+
{
12+
public interface IReplaceOpProcessor<T> : IOpProcessor
13+
where T : class, IIdentifiable<int>
14+
{ }
15+
16+
public interface IReplaceOpProcessor<T, TId> : IOpProcessor
17+
where T : class, IIdentifiable<TId>
18+
{ }
19+
20+
public class ReplaceOpProcessor<T> : ReplaceOpProcessor<T, int>
21+
where T : class, IIdentifiable<int>
22+
{
23+
public ReplaceOpProcessor(
24+
IUpdateService<T, int> service,
25+
IJsonApiDeSerializer deSerializer,
26+
IDocumentBuilder documentBuilder,
27+
IContextGraph contextGraph
28+
) : base(service, deSerializer, documentBuilder, contextGraph)
29+
{ }
30+
}
31+
32+
public class ReplaceOpProcessor<T, TId> : IReplaceOpProcessor<T, TId>
33+
where T : class, IIdentifiable<TId>
34+
{
35+
private readonly IUpdateService<T, TId> _service;
36+
private readonly IJsonApiDeSerializer _deSerializer;
37+
private readonly IDocumentBuilder _documentBuilder;
38+
private readonly IContextGraph _contextGraph;
39+
40+
public ReplaceOpProcessor(
41+
IUpdateService<T, TId> service,
42+
IJsonApiDeSerializer deSerializer,
43+
IDocumentBuilder documentBuilder,
44+
IContextGraph contextGraph)
45+
{
46+
_service = service;
47+
_deSerializer = deSerializer;
48+
_documentBuilder = documentBuilder;
49+
_contextGraph = contextGraph;
50+
}
51+
52+
public async Task<Operation> ProcessAsync(Operation operation)
53+
{
54+
Console.WriteLine(JsonConvert.SerializeObject(operation));
55+
var model = (T)_deSerializer.DocumentToObject(operation.DataObject);
56+
57+
if (string.IsNullOrWhiteSpace(operation?.DataObject?.Id?.ToString()))
58+
throw new JsonApiException(400, "The data.id parameter is required for replace operations");
59+
60+
var id = TypeHelper.ConvertType<TId>(operation.DataObject.Id);
61+
var result = await _service.UpdateAsync(id, model);
62+
63+
var operationResult = new Operation
64+
{
65+
Op = OperationCode.replace
66+
};
67+
68+
operationResult.Data = _documentBuilder.GetData(
69+
_contextGraph.GetContextEntity(operation.GetResourceTypeName()),
70+
result);
71+
72+
return operationResult;
73+
}
74+
}
75+
}

test/OperationsExampleTests/Get/GetTests.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
using System.Linq;
33
using System.Net;
44
using System.Threading.Tasks;
5+
using Bogus;
56
using JsonApiDotNetCore.Models.Operations;
6-
using Microsoft.EntityFrameworkCore;
77
using OperationsExample.Data;
8+
using OperationsExampleTests.Factories;
89
using Xunit;
910

1011
namespace OperationsExampleTests
@@ -13,6 +14,7 @@ namespace OperationsExampleTests
1314
public class GetTests
1415
{
1516
private readonly Fixture _fixture;
17+
private readonly Faker _faker = new Faker();
1618

1719
public GetTests(Fixture fixture)
1820
{
@@ -23,8 +25,12 @@ public GetTests(Fixture fixture)
2325
public async Task Can_Get_Articles()
2426
{
2527
// arrange
28+
var expectedCount = _faker.Random.Int(1, 10);
2629
var context = _fixture.GetService<AppDbContext>();
27-
var articles = await context.Articles.ToListAsync();
30+
context.Articles.RemoveRange(context.Articles);
31+
var articles = ArticleFactory.Get(expectedCount);
32+
context.AddRange(articles);
33+
context.SaveChanges();
2834

2935
var content = new
3036
{
@@ -44,15 +50,17 @@ public async Task Can_Get_Articles()
4450
Assert.NotNull(result.data);
4551
Assert.Equal(HttpStatusCode.OK, result.response.StatusCode);
4652
Assert.Equal(1, result.data.Operations.Count);
47-
Assert.Equal(articles.Count, result.data.Operations.Single().DataList.Count);
53+
Assert.Equal(expectedCount, result.data.Operations.Single().DataList.Count);
4854
}
4955

5056
[Fact]
5157
public async Task Can_Get_Article_By_Id()
5258
{
5359
// arrange
5460
var context = _fixture.GetService<AppDbContext>();
55-
var article = await context.Articles.LastAsync();
61+
var article = ArticleFactory.Get();
62+
context.Articles.Add(article);
63+
context.SaveChanges();
5664

5765
var content = new
5866
{
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Net;
4+
using System.Threading.Tasks;
5+
using Bogus;
6+
using JsonApiDotNetCore.Models.Operations;
7+
using OperationsExample.Data;
8+
using OperationsExampleTests.Factories;
9+
using Xunit;
10+
11+
namespace OperationsExampleTests
12+
{
13+
[Collection("WebHostCollection")]
14+
public class ReplaceTests
15+
{
16+
private readonly Fixture _fixture;
17+
private readonly Faker _faker = new Faker();
18+
19+
public ReplaceTests(Fixture fixture)
20+
{
21+
_fixture = fixture;
22+
}
23+
24+
[Fact]
25+
public async Task Can_Update_Article()
26+
{
27+
// arrange
28+
var context = _fixture.GetService<AppDbContext>();
29+
var article = ArticleFactory.Get();
30+
var updates = ArticleFactory.Get();
31+
context.Articles.Add(article);
32+
context.SaveChanges();
33+
34+
var content = new
35+
{
36+
operations = new[] {
37+
new {
38+
op = "replace",
39+
data = new {
40+
type = "articles",
41+
id = article.Id,
42+
attributes = new {
43+
name = updates.Name
44+
}
45+
}
46+
},
47+
}
48+
};
49+
50+
// act
51+
var result = await _fixture.PatchAsync<OperationsDocument>("api/bulk", content);
52+
53+
// assert
54+
Assert.NotNull(result.response);
55+
Assert.NotNull(result.data);
56+
Assert.Equal(HttpStatusCode.OK, result.response.StatusCode);
57+
Assert.Equal(1, result.data.Operations.Count);
58+
59+
var attrs = result.data.Operations.Single().DataObject.Attributes;
60+
Assert.Equal(updates.Name, attrs["name"]);
61+
}
62+
63+
[Fact]
64+
public async Task Can_Update_Articles()
65+
{
66+
// arrange
67+
var count = _faker.Random.Int(1, 10);
68+
var context = _fixture.GetService<AppDbContext>();
69+
70+
var articles = ArticleFactory.Get(count);
71+
var updates = ArticleFactory.Get(count);
72+
73+
context.Articles.AddRange(articles);
74+
context.SaveChanges();
75+
76+
var content = new
77+
{
78+
operations = new List<object>()
79+
};
80+
81+
for (int i = 0; i < count; i++)
82+
content.operations.Add(new
83+
{
84+
op = "replace",
85+
data = new
86+
{
87+
type = "articles",
88+
id = articles[i].Id,
89+
attributes = new
90+
{
91+
name = updates[i].Name
92+
}
93+
}
94+
});
95+
96+
// act
97+
var result = await _fixture.PatchAsync<OperationsDocument>("api/bulk", content);
98+
99+
// assert
100+
Assert.NotNull(result.response);
101+
Assert.NotNull(result.data);
102+
Assert.Equal(HttpStatusCode.OK, result.response.StatusCode);
103+
Assert.Equal(count, result.data.Operations.Count);
104+
105+
for (int i = 0; i < count; i++)
106+
{
107+
var attrs = result.data.Operations[i].DataObject.Attributes;
108+
Assert.Equal(updates[i].Name, attrs["name"]);
109+
}
110+
}
111+
}
112+
}

test/OperationsExampleTests/WebHostCollection.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212
namespace OperationsExampleTests
1313
{
1414
[CollectionDefinition("WebHostCollection")]
15-
public class WebHostCollection
16-
: ICollectionFixture<Fixture>
15+
public class WebHostCollection : ICollectionFixture<Fixture>
1716
{ }
1817

1918
public class Fixture

0 commit comments

Comments
 (0)