Skip to content

Commit 63a1455

Browse files
author
Bart Koelman
committed
Soft-deletion tests, done right this time!
1 parent 0839529 commit 63a1455

File tree

8 files changed

+982
-207
lines changed

8 files changed

+982
-207
lines changed

test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/Company.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23
using JetBrains.Annotations;
34
using JsonApiDotNetCore.Resources;
@@ -11,8 +12,7 @@ public sealed class Company : Identifiable, ISoftDeletable
1112
[Attr]
1213
public string Name { get; set; }
1314

14-
[Attr]
15-
public bool IsSoftDeleted { get; set; }
15+
public DateTimeOffset? SoftDeletedAt { get; set; }
1616

1717
[HasMany]
1818
public ICollection<Department> Departments { get; set; }

test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/Department.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using JetBrains.Annotations;
23
using JsonApiDotNetCore.Resources;
34
using JsonApiDotNetCore.Resources.Annotations;
@@ -10,8 +11,7 @@ public sealed class Department : Identifiable, ISoftDeletable
1011
[Attr]
1112
public string Name { get; set; }
1213

13-
[Attr]
14-
public bool IsSoftDeleted { get; set; }
14+
public DateTimeOffset? SoftDeletedAt { get; set; }
1515

1616
[HasOne]
1717
public Company Company { get; set; }
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
using System;
2+
using JetBrains.Annotations;
3+
14
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.SoftDeletion
25
{
6+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
37
public interface ISoftDeletable
48
{
5-
bool IsSoftDeleted { get; set; }
9+
DateTimeOffset? SoftDeletedAt { get; set; }
610
}
711
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using JetBrains.Annotations;
7+
using JsonApiDotNetCore;
8+
using JsonApiDotNetCore.Configuration;
9+
using JsonApiDotNetCore.Errors;
10+
using JsonApiDotNetCore.Middleware;
11+
using JsonApiDotNetCore.Queries;
12+
using JsonApiDotNetCore.Repositories;
13+
using JsonApiDotNetCore.Resources;
14+
using JsonApiDotNetCore.Resources.Annotations;
15+
using Microsoft.AspNetCore.Authentication;
16+
using Microsoft.EntityFrameworkCore;
17+
using Microsoft.Extensions.Logging;
18+
19+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.SoftDeletion
20+
{
21+
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
22+
public class SoftDeletionAwareRepository<TResource, TId> : EntityFrameworkCoreRepository<TResource, TId>
23+
where TResource : class, IIdentifiable<TId>
24+
{
25+
private readonly CollectionConverter _collectionConverter = new CollectionConverter();
26+
private readonly IJsonApiRequest _request;
27+
private readonly ISystemClock _systemClock;
28+
private readonly ITargetedFields _targetedFields;
29+
private readonly DbContext _dbContext;
30+
31+
public SoftDeletionAwareRepository(IJsonApiRequest request, ISystemClock systemClock, ITargetedFields targetedFields,
32+
IDbContextResolver contextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory,
33+
IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory)
34+
: base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory)
35+
{
36+
_request = request;
37+
_systemClock = systemClock;
38+
_targetedFields = targetedFields;
39+
_dbContext = contextResolver.GetContext();
40+
}
41+
42+
public override async Task CreateAsync(TResource resourceFromRequest, TResource resourceForDatabase, CancellationToken cancellationToken)
43+
{
44+
await AssertResourcesToAssignInRelationshipsExistAsync(resourceFromRequest, cancellationToken);
45+
46+
await base.CreateAsync(resourceFromRequest, resourceForDatabase, cancellationToken);
47+
}
48+
49+
public override async Task UpdateAsync(TResource resourceFromRequest, TResource resourceFromDatabase, CancellationToken cancellationToken)
50+
{
51+
await AssertResourcesToAssignInRelationshipsExistAsync(resourceFromRequest, cancellationToken);
52+
53+
await base.UpdateAsync(resourceFromRequest, resourceFromDatabase, cancellationToken);
54+
}
55+
56+
public override async Task SetRelationshipAsync(TResource primaryResource, object secondaryResourceIds, CancellationToken cancellationToken)
57+
{
58+
await AssertRelationshipRightValueExistsAsync(_request.Relationship, secondaryResourceIds, cancellationToken);
59+
60+
await base.SetRelationshipAsync(primaryResource, secondaryResourceIds, cancellationToken);
61+
}
62+
63+
public override async Task AddToToManyRelationshipAsync(TId primaryId, ISet<IIdentifiable> secondaryResourceIds, CancellationToken cancellationToken)
64+
{
65+
if (IsSoftDeletable(typeof(TResource)))
66+
{
67+
await AssertPrimaryResourceExistsAsync(primaryId, cancellationToken);
68+
}
69+
70+
await AssertRelationshipRightValueExistsAsync(_request.Relationship, secondaryResourceIds, cancellationToken);
71+
72+
await base.AddToToManyRelationshipAsync(primaryId, secondaryResourceIds, cancellationToken);
73+
}
74+
75+
private async Task AssertResourcesToAssignInRelationshipsExistAsync(TResource resourceFromRequest, CancellationToken cancellationToken)
76+
{
77+
foreach (RelationshipAttribute relationship in _targetedFields.Relationships)
78+
{
79+
object rightValue = relationship.GetValue(resourceFromRequest);
80+
await AssertRelationshipRightValueExistsAsync(relationship, rightValue, cancellationToken);
81+
}
82+
}
83+
84+
private async Task AssertRelationshipRightValueExistsAsync(RelationshipAttribute relationship, object rightValue, CancellationToken cancellationToken)
85+
{
86+
if (IsSoftDeletable(relationship.RightType))
87+
{
88+
ICollection<IIdentifiable> rightResources = _collectionConverter.ExtractResources(rightValue);
89+
object[] rightResourceIds = rightResources.Select(identifiable => identifiable.GetTypedId()).ToArray();
90+
91+
foreach (object rightResourceId in rightResourceIds)
92+
{
93+
object resource = await _dbContext.FindAsync(relationship.RightType, ArrayFactory.Create(rightResourceId), cancellationToken);
94+
95+
if (resource == null)
96+
{
97+
throw new DataStoreUpdateException(new Exception("One or more related resources do not exist."));
98+
}
99+
}
100+
}
101+
}
102+
103+
private async Task AssertPrimaryResourceExistsAsync(TId primaryId, CancellationToken cancellationToken)
104+
{
105+
_ = await GetPrimaryResourceByIdAsync(primaryId, cancellationToken);
106+
}
107+
108+
public override async Task DeleteAsync(TId id, CancellationToken cancellationToken)
109+
{
110+
if (IsSoftDeletable(typeof(TResource)))
111+
{
112+
await SoftDeleteAsync(id, cancellationToken);
113+
}
114+
else
115+
{
116+
await base.DeleteAsync(id, cancellationToken);
117+
}
118+
}
119+
120+
private async Task SoftDeleteAsync(TId id, CancellationToken cancellationToken)
121+
{
122+
var resource = (ISoftDeletable)await GetPrimaryResourceByIdAsync(id, cancellationToken);
123+
124+
if (resource == null)
125+
{
126+
throw new ResourceNotFoundException(_request.PrimaryId, _request.PrimaryResource.PublicName);
127+
}
128+
129+
resource.SoftDeletedAt = _systemClock.UtcNow;
130+
131+
await _dbContext.SaveChangesAsync(cancellationToken);
132+
}
133+
134+
private async Task<TResource> GetPrimaryResourceByIdAsync(TId id, CancellationToken cancellationToken)
135+
{
136+
var resource = await _dbContext.FindAsync<TResource>(ArrayFactory.Create((object)id), cancellationToken);
137+
138+
if (resource == null)
139+
{
140+
throw new ResourceNotFoundException(_request.PrimaryId, _request.PrimaryResource.PublicName);
141+
}
142+
143+
return resource;
144+
}
145+
146+
private static bool IsSoftDeletable(Type resourceType)
147+
{
148+
return typeof(ISoftDeletable).IsAssignableFrom(resourceType);
149+
}
150+
}
151+
152+
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
153+
public class SoftDeletionAwareRepository<TResource> : SoftDeletionAwareRepository<TResource, int>, IResourceRepository<TResource>
154+
where TResource : class, IIdentifiable<int>
155+
{
156+
public SoftDeletionAwareRepository(IJsonApiRequest request, ISystemClock systemClock, ITargetedFields targetedFields,
157+
IDbContextResolver contextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory,
158+
IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory)
159+
: base(request, systemClock, targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory)
160+
{
161+
}
162+
}
163+
}

test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionDbContext.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using JetBrains.Annotations;
22
using Microsoft.EntityFrameworkCore;
33

4+
// @formatter:wrap_chained_method_calls chop_always
5+
46
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.SoftDeletion
57
{
68
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
@@ -13,5 +15,14 @@ public SoftDeletionDbContext(DbContextOptions<SoftDeletionDbContext> options)
1315
: base(options)
1416
{
1517
}
18+
19+
protected override void OnModelCreating(ModelBuilder builder)
20+
{
21+
builder.Entity<Company>()
22+
.HasQueryFilter(company => company.SoftDeletedAt == null);
23+
24+
builder.Entity<Department>()
25+
.HasQueryFilter(department => department.SoftDeletedAt == null);
26+
}
1627
}
1728
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using Bogus;
3+
using TestBuildingBlocks;
4+
5+
// @formatter:wrap_chained_method_calls chop_always
6+
// @formatter:keep_existing_linebreaks true
7+
8+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.SoftDeletion
9+
{
10+
internal sealed class SoftDeletionFakers : FakerContainer
11+
{
12+
private readonly Lazy<Faker<Company>> _lazyCompanyFaker = new Lazy<Faker<Company>>(() =>
13+
new Faker<Company>()
14+
.UseSeed(GetFakerSeed())
15+
.RuleFor(company => company.Name, faker => faker.Company.CompanyName()));
16+
17+
private readonly Lazy<Faker<Department>> _lazyDepartmentFaker = new Lazy<Faker<Department>>(() =>
18+
new Faker<Department>()
19+
.UseSeed(GetFakerSeed())
20+
.RuleFor(department => department.Name, faker => faker.Lorem.Word()));
21+
22+
public Faker<Company> Company => _lazyCompanyFaker.Value;
23+
public Faker<Department> Department => _lazyDepartmentFaker.Value;
24+
}
25+
}

test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionResourceDefinition.cs

Lines changed: 0 additions & 38 deletions
This file was deleted.

0 commit comments

Comments
 (0)