Skip to content

Commit 7471aa5

Browse files
committed
remove operations
1 parent 69323c6 commit 7471aa5

File tree

6 files changed

+187
-3
lines changed

6 files changed

+187
-3
lines changed

src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ public static void AddJsonApiInternals(
113113
services.AddScoped(typeof(IUpdateService<>), typeof(EntityResourceService<>));
114114
services.AddScoped(typeof(IUpdateService<,>), typeof(EntityResourceService<,>));
115115

116+
services.AddScoped(typeof(IDeleteService<>), typeof(EntityResourceService<>));
117+
services.AddScoped(typeof(IDeleteService<,>), typeof(EntityResourceService<,>));
118+
116119
services.AddScoped(typeof(IResourceService<>), typeof(EntityResourceService<>));
117120
services.AddScoped(typeof(IResourceService<,>), typeof(EntityResourceService<,>));
118121
services.AddSingleton<JsonApiOptions>(jsonApiOptions);
@@ -148,6 +151,9 @@ private static void AddOperationServices(IServiceCollection services)
148151
services.AddScoped(typeof(IReplaceOpProcessor<>), typeof(ReplaceOpProcessor<>));
149152
services.AddScoped(typeof(IReplaceOpProcessor<,>), typeof(ReplaceOpProcessor<,>));
150153

154+
services.AddScoped(typeof(IRemoveOpProcessor<>), typeof(RemoveOpProcessor<>));
155+
services.AddScoped(typeof(IRemoveOpProcessor<,>), typeof(RemoveOpProcessor<,>));
156+
151157
services.AddSingleton<IOperationProcessorResolver, OperationProcessorResolver>();
152158
services.AddSingleton<IGenericProcessorFactory, GenericProcessorFactory>();
153159
}

src/JsonApiDotNetCore/Services/Operations/OperationProcessorResolver.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,20 @@ public interface IOperationProcessorResolver
1010
IOpProcessor LocateCreateService(Operation operation);
1111
IOpProcessor LocateGetService(Operation operation);
1212
IOpProcessor LocateReplaceService(Operation operation);
13+
IOpProcessor LocateRemoveService(Operation operation);
1314
}
1415

1516
public class OperationProcessorResolver : IOperationProcessorResolver
1617
{
1718
private readonly IGenericProcessorFactory _processorFactory;
1819
private readonly IJsonApiContext _context;
20+
21+
// processor caches -- since there is some associated cost with creating the processors, we store them in memory
22+
// to reduce the cost of subsequent requests. in the future, this may be moved into setup code run at startup
1923
private ConcurrentDictionary<string, IOpProcessor> _createOpProcessors = new ConcurrentDictionary<string, IOpProcessor>();
2024
private ConcurrentDictionary<string, IOpProcessor> _getOpProcessors = new ConcurrentDictionary<string, IOpProcessor>();
2125
private ConcurrentDictionary<string, IOpProcessor> _replaceOpProcessors = new ConcurrentDictionary<string, IOpProcessor>();
26+
private ConcurrentDictionary<string, IOpProcessor> _removeOpProcessors = new ConcurrentDictionary<string, IOpProcessor>();
2227

2328
public OperationProcessorResolver(
2429
IGenericProcessorFactory processorFactory,
@@ -80,5 +85,22 @@ public IOpProcessor LocateReplaceService(Operation operation)
8085

8186
return processor;
8287
}
88+
89+
public IOpProcessor LocateRemoveService(Operation operation)
90+
{
91+
var resource = operation.GetResourceTypeName();
92+
93+
if (_removeOpProcessors.TryGetValue(resource, out IOpProcessor cachedProcessor))
94+
return cachedProcessor;
95+
96+
var contextEntity = _context.ContextGraph.GetContextEntity(resource);
97+
var processor = _processorFactory.GetProcessor<IOpProcessor>(
98+
typeof(IRemoveOpProcessor<,>), contextEntity.EntityType, contextEntity.IdentityType
99+
);
100+
101+
_removeOpProcessors[resource] = processor;
102+
103+
return processor;
104+
}
83105
}
84106
}

src/JsonApiDotNetCore/Services/Operations/OperationsProcessor.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ private IOpProcessor GetOperationsProcessor(Operation op)
7171
return _processorResolver.LocateGetService(op);
7272
case OperationCode.replace:
7373
return _processorResolver.LocateReplaceService(op);
74+
case OperationCode.remove:
75+
return _processorResolver.LocateRemoveService(op);
7476
default:
7577
throw new JsonApiException(400, $"'{op.Op}' is not a valid operation code");
7678
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
9+
namespace JsonApiDotNetCore.Services.Operations.Processors
10+
{
11+
public interface IRemoveOpProcessor<T> : IOpProcessor
12+
where T : class, IIdentifiable<int>
13+
{ }
14+
15+
public interface IRemoveOpProcessor<T, TId> : IOpProcessor
16+
where T : class, IIdentifiable<TId>
17+
{ }
18+
19+
public class RemoveOpProcessor<T> : RemoveOpProcessor<T, int>
20+
where T : class, IIdentifiable<int>
21+
{
22+
public RemoveOpProcessor(
23+
IDeleteService<T, int> service,
24+
IJsonApiDeSerializer deSerializer,
25+
IDocumentBuilder documentBuilder,
26+
IContextGraph contextGraph
27+
) : base(service, deSerializer, documentBuilder, contextGraph)
28+
{ }
29+
}
30+
31+
public class RemoveOpProcessor<T, TId> : IRemoveOpProcessor<T, TId>
32+
where T : class, IIdentifiable<TId>
33+
{
34+
private readonly IDeleteService<T, TId> _service;
35+
private readonly IJsonApiDeSerializer _deSerializer;
36+
private readonly IDocumentBuilder _documentBuilder;
37+
private readonly IContextGraph _contextGraph;
38+
39+
public RemoveOpProcessor(
40+
IDeleteService<T, TId> service,
41+
IJsonApiDeSerializer deSerializer,
42+
IDocumentBuilder documentBuilder,
43+
IContextGraph contextGraph)
44+
{
45+
_service = service;
46+
_deSerializer = deSerializer;
47+
_documentBuilder = documentBuilder;
48+
_contextGraph = contextGraph;
49+
}
50+
51+
public async Task<Operation> ProcessAsync(Operation operation)
52+
{
53+
var stringId = operation.Ref?.Id?.ToString();
54+
if (string.IsNullOrWhiteSpace(stringId))
55+
throw new JsonApiException(400, "The data.id parameter is required for delete operations");
56+
57+
var id = TypeHelper.ConvertType<TId>(stringId);
58+
var result = await _service.DeleteAsync(id);
59+
60+
var operationResult = new Operation { };
61+
62+
return operationResult;
63+
}
64+
}
65+
}

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
using System;
21
using System.Threading.Tasks;
32
using JsonApiDotNetCore.Builders;
43
using JsonApiDotNetCore.Internal;
54
using JsonApiDotNetCore.Models;
65
using JsonApiDotNetCore.Models.Operations;
76
using JsonApiDotNetCore.Serialization;
8-
using Newtonsoft.Json;
97

108
namespace JsonApiDotNetCore.Services.Operations.Processors
119
{
@@ -51,7 +49,6 @@ public ReplaceOpProcessor(
5149

5250
public async Task<Operation> ProcessAsync(Operation operation)
5351
{
54-
Console.WriteLine(JsonConvert.SerializeObject(operation));
5552
var model = (T)_deSerializer.DocumentToObject(operation.DataObject);
5653

5754
if (string.IsNullOrWhiteSpace(operation?.DataObject?.Id?.ToString()))
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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 RemoveTests
15+
{
16+
private readonly Fixture _fixture;
17+
private readonly Faker _faker = new Faker();
18+
19+
public RemoveTests(Fixture fixture)
20+
{
21+
_fixture = fixture;
22+
}
23+
24+
[Fact]
25+
public async Task Can_Remove_Article()
26+
{
27+
// arrange
28+
var context = _fixture.GetService<AppDbContext>();
29+
var article = ArticleFactory.Get();
30+
context.Articles.Add(article);
31+
context.SaveChanges();
32+
33+
var content = new
34+
{
35+
operations = new[] {
36+
new Dictionary<string, object> {
37+
{ "op", "remove"},
38+
{ "ref", new { type = "articles", id = article.StringId } }
39+
}
40+
}
41+
};
42+
43+
// act
44+
var result = await _fixture.PatchAsync<OperationsDocument>("api/bulk", content);
45+
46+
// assert
47+
Assert.NotNull(result.response);
48+
Assert.NotNull(result.data);
49+
Assert.Equal(HttpStatusCode.OK, result.response.StatusCode);
50+
Assert.Equal(1, result.data.Operations.Count);
51+
Assert.Null(context.Articles.SingleOrDefault(a => a.Id == article.Id));
52+
}
53+
54+
[Fact]
55+
public async Task Can_Remove_Articles()
56+
{
57+
// arrange
58+
var count = _faker.Random.Int(1, 10);
59+
var context = _fixture.GetService<AppDbContext>();
60+
61+
var articles = ArticleFactory.Get(count);
62+
63+
context.Articles.AddRange(articles);
64+
context.SaveChanges();
65+
66+
var content = new
67+
{
68+
operations = new List<object>()
69+
};
70+
71+
for (int i = 0; i < count; i++)
72+
content.operations.Add(
73+
new Dictionary<string, object> {
74+
{ "op", "remove"},
75+
{ "ref", new { type = "articles", id = articles[i].StringId } }
76+
}
77+
);
78+
79+
// act
80+
var result = await _fixture.PatchAsync<OperationsDocument>("api/bulk", content);
81+
82+
// assert
83+
Assert.NotNull(result.response);
84+
Assert.NotNull(result.data);
85+
Assert.Equal(HttpStatusCode.OK, result.response.StatusCode);
86+
Assert.Equal(count, result.data.Operations.Count);
87+
88+
for (int i = 0; i < count; i++)
89+
Assert.Null(context.Articles.SingleOrDefault(a => a.Id == articles[i].Id));
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)