Skip to content

Commit 68063dc

Browse files
author
Bart Koelman
committed
Resource inheritance: write endpoints
1 parent 4d34dcd commit 68063dc

37 files changed

+3452
-120
lines changed

Diff for: benchmarks/Serialization/SerializationBenchmarkBase.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,9 @@ public Task OnSetToManyRelationshipAsync<TResource>(TResource leftResource, HasM
180180
return Task.CompletedTask;
181181
}
182182

183-
public Task OnAddToRelationshipAsync<TResource, TId>(TId leftResourceId, HasManyAttribute hasManyRelationship, ISet<IIdentifiable> rightResourceIds,
183+
public Task OnAddToRelationshipAsync<TResource>(TResource leftResource, HasManyAttribute hasManyRelationship, ISet<IIdentifiable> rightResourceIds,
184184
CancellationToken cancellationToken)
185-
where TResource : class, IIdentifiable<TId>
185+
where TResource : class, IIdentifiable
186186
{
187187
return Task.CompletedTask;
188188
}

Diff for: src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs

+42
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,43 @@ public IReadOnlySet<ResourceType> GetAllConcreteDerivedTypes()
206206
return _lazyAllConcreteDerivedTypes.Value;
207207
}
208208

209+
/// <summary>
210+
/// Searches the tree of derived types to find a match for the specified <paramref name="clrType" />.
211+
/// </summary>
212+
public ResourceType GetTypeOrDerived(Type clrType)
213+
{
214+
ArgumentGuard.NotNull(clrType, nameof(clrType));
215+
216+
ResourceType? derivedType = FindTypeOrDerived(this, clrType);
217+
218+
if (derivedType == null)
219+
{
220+
throw new InvalidOperationException($"Resource type '{PublicName}' is not a base type of '{clrType}'.");
221+
}
222+
223+
return derivedType;
224+
}
225+
226+
private static ResourceType? FindTypeOrDerived(ResourceType type, Type clrType)
227+
{
228+
if (type.ClrType == clrType)
229+
{
230+
return type;
231+
}
232+
233+
foreach (ResourceType derivedType in type.DirectlyDerivedTypes)
234+
{
235+
ResourceType? matchingType = FindTypeOrDerived(derivedType, clrType);
236+
237+
if (matchingType != null)
238+
{
239+
return matchingType;
240+
}
241+
}
242+
243+
return null;
244+
}
245+
209246
internal IReadOnlySet<AttrAttribute> GetAttributesInTypeOrDerived(string publicName)
210247
{
211248
return GetAttributesInTypeOrDerived(this, publicName);
@@ -261,6 +298,11 @@ private static IReadOnlySet<RelationshipAttribute> GetRelationshipsInTypeOrDeriv
261298
return relationshipsInDerivedTypes;
262299
}
263300

301+
internal bool IsPartOfTypeHierarchy()
302+
{
303+
return BaseType != null || DirectlyDerivedTypes.Any();
304+
}
305+
264306
public override string ToString()
265307
{
266308
return PublicName;

Diff for: src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ public QueryLayer ComposeForGetRelationshipRightIds(RelationshipAttribute relati
422422

423423
AttrAttribute rightIdAttribute = GetIdAttribute(relationship.RightType);
424424

425-
object[] typedIds = rightResourceIds.Select(resource => resource.GetTypedId()).ToArray();
425+
HashSet<object> typedIds = rightResourceIds.Select(resource => resource.GetTypedId()).ToHashSet();
426426

427427
FilterExpression? baseFilter = GetFilter(Array.Empty<QueryExpression>(), relationship.RightType);
428428
FilterExpression? filter = CreateFilterByIds(typedIds, rightIdAttribute, baseFilter);
@@ -447,7 +447,7 @@ public QueryLayer ComposeForHasMany<TId>(HasManyAttribute hasManyRelationship, T
447447

448448
AttrAttribute leftIdAttribute = GetIdAttribute(hasManyRelationship.LeftType);
449449
AttrAttribute rightIdAttribute = GetIdAttribute(hasManyRelationship.RightType);
450-
object[] rightTypedIds = rightResourceIds.Select(resource => resource.GetTypedId()).ToArray();
450+
HashSet<object> rightTypedIds = rightResourceIds.Select(resource => resource.GetTypedId()).ToHashSet();
451451

452452
FilterExpression? leftFilter = CreateFilterByIds(leftId.AsArray(), leftIdAttribute, null);
453453
FilterExpression? rightFilter = CreateFilterByIds(rightTypedIds, rightIdAttribute, null);

Diff for: src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs

+45-13
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,15 @@ protected virtual IQueryable<TResource> GetAll()
155155
}
156156

157157
/// <inheritdoc />
158-
public virtual Task<TResource> GetForCreateAsync(TId id, CancellationToken cancellationToken)
158+
public virtual Task<TResource> GetForCreateAsync(Type resourceClrType, TId id, CancellationToken cancellationToken)
159159
{
160160
_traceWriter.LogMethodStart(new
161161
{
162+
resourceClrType,
162163
id
163164
});
164165

165-
var resource = _resourceFactory.CreateInstance<TResource>();
166+
var resource = (TResource)_resourceFactory.CreateInstance(resourceClrType);
166167
resource.Id = id;
167168

168169
return Task.FromResult(resource);
@@ -305,18 +306,19 @@ protected void AssertIsNotClearingRequiredToOneRelationship(RelationshipAttribut
305306
}
306307

307308
/// <inheritdoc />
308-
public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToken)
309+
public virtual async Task DeleteAsync(TResource? resourceFromDatabase, TId id, CancellationToken cancellationToken)
309310
{
310311
_traceWriter.LogMethodStart(new
311312
{
313+
resourceFromDatabase,
312314
id
313315
});
314316

315317
using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Delete resource");
316318

317319
// This enables OnWritingAsync() to fetch the resource, which adds it to the change tracker.
318320
// If so, we'll reuse the tracked resource instead of this placeholder resource.
319-
var placeholderResource = _resourceFactory.CreateInstance<TResource>();
321+
TResource placeholderResource = resourceFromDatabase ?? _resourceFactory.CreateInstance<TResource>();
320322
placeholderResource.Id = id;
321323

322324
await _resourceDefinitionAccessor.OnWritingAsync(placeholderResource, WriteOperationKind.DeleteResource, cancellationToken);
@@ -413,10 +415,12 @@ public virtual async Task SetRelationshipAsync(TResource leftResource, object? r
413415
}
414416

415417
/// <inheritdoc />
416-
public virtual async Task AddToToManyRelationshipAsync(TId leftId, ISet<IIdentifiable> rightResourceIds, CancellationToken cancellationToken)
418+
public virtual async Task AddToToManyRelationshipAsync(TResource? leftResource, TId leftId, ISet<IIdentifiable> rightResourceIds,
419+
CancellationToken cancellationToken)
417420
{
418421
_traceWriter.LogMethodStart(new
419422
{
423+
leftResource,
420424
leftId,
421425
rightResourceIds
422426
});
@@ -427,25 +431,53 @@ public virtual async Task AddToToManyRelationshipAsync(TId leftId, ISet<IIdentif
427431

428432
var relationship = (HasManyAttribute)_targetedFields.Relationships.Single();
429433

430-
await _resourceDefinitionAccessor.OnAddToRelationshipAsync<TResource, TId>(leftId, relationship, rightResourceIds, cancellationToken);
434+
// This enables OnAddToRelationshipAsync() or OnWritingAsync() to fetch the resource, which adds it to the change tracker.
435+
// If so, we'll reuse the tracked resource instead of this placeholder resource.
436+
TResource leftPlaceholderResource = leftResource ?? _resourceFactory.CreateInstance<TResource>();
437+
leftPlaceholderResource.Id = leftId;
438+
439+
await _resourceDefinitionAccessor.OnAddToRelationshipAsync(leftPlaceholderResource, relationship, rightResourceIds, cancellationToken);
431440

432441
if (rightResourceIds.Any())
433442
{
434-
var leftPlaceholderResource = _resourceFactory.CreateInstance<TResource>();
435-
leftPlaceholderResource.Id = leftId;
436-
437443
var leftResourceTracked = (TResource)_dbContext.GetTrackedOrAttach(leftPlaceholderResource);
444+
IEnumerable rightValueToStore = GetRightValueToStoreForAddToToMany(leftResourceTracked, relationship, rightResourceIds);
438445

439-
await UpdateRelationshipAsync(relationship, leftResourceTracked, rightResourceIds, cancellationToken);
446+
await UpdateRelationshipAsync(relationship, leftResourceTracked, rightValueToStore, cancellationToken);
440447

441448
await _resourceDefinitionAccessor.OnWritingAsync(leftResourceTracked, WriteOperationKind.AddToRelationship, cancellationToken);
449+
leftResourceTracked = (TResource)_dbContext.GetTrackedOrAttach(leftResourceTracked);
442450

443451
await SaveChangesAsync(cancellationToken);
444452

445453
await _resourceDefinitionAccessor.OnWriteSucceededAsync(leftResourceTracked, WriteOperationKind.AddToRelationship, cancellationToken);
446454
}
447455
}
448456

457+
private IEnumerable GetRightValueToStoreForAddToToMany(TResource leftResource, HasManyAttribute relationship, ISet<IIdentifiable> rightResourceIdsToAdd)
458+
{
459+
object? rightValueStored = relationship.GetValue(leftResource);
460+
461+
// @formatter:wrap_chained_method_calls chop_always
462+
// @formatter:keep_existing_linebreaks true
463+
464+
HashSet<IIdentifiable> rightResourceIdsStored = _collectionConverter
465+
.ExtractResources(rightValueStored)
466+
.Select(rightResource => _dbContext.GetTrackedOrAttach(rightResource))
467+
.ToHashSet(IdentifiableComparer.Instance);
468+
469+
// @formatter:keep_existing_linebreaks restore
470+
// @formatter:wrap_chained_method_calls restore
471+
472+
if (rightResourceIdsStored.Any())
473+
{
474+
rightResourceIdsStored.AddRange(rightResourceIdsToAdd);
475+
return rightResourceIdsStored;
476+
}
477+
478+
return rightResourceIdsToAdd;
479+
}
480+
449481
/// <inheritdoc />
450482
public virtual async Task RemoveFromToManyRelationshipAsync(TResource leftResource, ISet<IIdentifiable> rightResourceIds,
451483
CancellationToken cancellationToken)
@@ -473,7 +505,7 @@ public virtual async Task RemoveFromToManyRelationshipAsync(TResource leftResour
473505
// Make Entity Framework Core believe any additional resources added from ResourceDefinition already exist in database.
474506
IIdentifiable[] extraResourceIdsToRemove = rightResourceIdsToRemove.Where(rightId => !rightResourceIds.Contains(rightId)).ToArray();
475507

476-
object? rightValueStored = relationship.GetValue(leftResource);
508+
object? rightValueStored = relationship.GetValue(leftResourceTracked);
477509

478510
// @formatter:wrap_chained_method_calls chop_always
479511
// @formatter:keep_existing_linebreaks true
@@ -488,9 +520,9 @@ public virtual async Task RemoveFromToManyRelationshipAsync(TResource leftResour
488520
// @formatter:wrap_chained_method_calls restore
489521

490522
rightValueStored = _collectionConverter.CopyToTypedCollection(rightResourceIdsStored, relationship.Property.PropertyType);
491-
relationship.SetValue(leftResource, rightValueStored);
523+
relationship.SetValue(leftResourceTracked, rightValueStored);
492524

493-
MarkRelationshipAsLoaded(leftResource, relationship);
525+
MarkRelationshipAsLoaded(leftResourceTracked, relationship);
494526

495527
HashSet<IIdentifiable> rightResourceIdsToStore = rightResourceIdsStored.ToHashSet(IdentifiableComparer.Instance);
496528
rightResourceIdsToStore.ExceptWith(rightResourceIdsToRemove);

Diff for: src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs

+11-10
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace JsonApiDotNetCore.Repositories;
1111
public interface IResourceRepositoryAccessor
1212
{
1313
/// <summary>
14-
/// Invokes <see cref="IResourceReadRepository{TResource,TId}.GetAsync" />.
14+
/// Invokes <see cref="IResourceReadRepository{TResource,TId}.GetAsync" /> for the specified resource type.
1515
/// </summary>
1616
Task<IReadOnlyCollection<TResource>> GetAsync<TResource>(QueryLayer queryLayer, CancellationToken cancellationToken)
1717
where TResource : class, IIdentifiable;
@@ -27,49 +27,50 @@ Task<IReadOnlyCollection<TResource>> GetAsync<TResource>(QueryLayer queryLayer,
2727
Task<int> CountAsync(ResourceType resourceType, FilterExpression? filter, CancellationToken cancellationToken);
2828

2929
/// <summary>
30-
/// Invokes <see cref="IResourceWriteRepository{TResource,TId}.GetForCreateAsync" />.
30+
/// Invokes <see cref="IResourceWriteRepository{TResource,TId}.GetForCreateAsync" /> for the specified resource type.
3131
/// </summary>
32-
Task<TResource> GetForCreateAsync<TResource, TId>(TId id, CancellationToken cancellationToken)
32+
Task<TResource> GetForCreateAsync<TResource, TId>(Type resourceClrType, TId id, CancellationToken cancellationToken)
3333
where TResource : class, IIdentifiable<TId>;
3434

3535
/// <summary>
36-
/// Invokes <see cref="IResourceWriteRepository{TResource,TId}.CreateAsync" />.
36+
/// Invokes <see cref="IResourceWriteRepository{TResource,TId}.CreateAsync" /> for the specified resource type.
3737
/// </summary>
3838
Task CreateAsync<TResource>(TResource resourceFromRequest, TResource resourceForDatabase, CancellationToken cancellationToken)
3939
where TResource : class, IIdentifiable;
4040

4141
/// <summary>
42-
/// Invokes <see cref="IResourceWriteRepository{TResource,TId}.GetForUpdateAsync" />.
42+
/// Invokes <see cref="IResourceWriteRepository{TResource,TId}.GetForUpdateAsync" /> for the specified resource type.
4343
/// </summary>
4444
Task<TResource?> GetForUpdateAsync<TResource>(QueryLayer queryLayer, CancellationToken cancellationToken)
4545
where TResource : class, IIdentifiable;
4646

4747
/// <summary>
48-
/// Invokes <see cref="IResourceWriteRepository{TResource,TId}.UpdateAsync" />.
48+
/// Invokes <see cref="IResourceWriteRepository{TResource,TId}.UpdateAsync" /> for the specified resource type.
4949
/// </summary>
5050
Task UpdateAsync<TResource>(TResource resourceFromRequest, TResource resourceFromDatabase, CancellationToken cancellationToken)
5151
where TResource : class, IIdentifiable;
5252

5353
/// <summary>
5454
/// Invokes <see cref="IResourceWriteRepository{TResource,TId}.DeleteAsync" /> for the specified resource type.
5555
/// </summary>
56-
Task DeleteAsync<TResource, TId>(TId id, CancellationToken cancellationToken)
56+
Task DeleteAsync<TResource, TId>(TResource? resourceFromDatabase, TId id, CancellationToken cancellationToken)
5757
where TResource : class, IIdentifiable<TId>;
5858

5959
/// <summary>
60-
/// Invokes <see cref="IResourceWriteRepository{TResource,TId}.SetRelationshipAsync" />.
60+
/// Invokes <see cref="IResourceWriteRepository{TResource,TId}.SetRelationshipAsync" /> for the specified resource type.
6161
/// </summary>
6262
Task SetRelationshipAsync<TResource>(TResource leftResource, object? rightValue, CancellationToken cancellationToken)
6363
where TResource : class, IIdentifiable;
6464

6565
/// <summary>
6666
/// Invokes <see cref="IResourceWriteRepository{TResource,TId}.AddToToManyRelationshipAsync" /> for the specified resource type.
6767
/// </summary>
68-
Task AddToToManyRelationshipAsync<TResource, TId>(TId leftId, ISet<IIdentifiable> rightResourceIds, CancellationToken cancellationToken)
68+
Task AddToToManyRelationshipAsync<TResource, TId>(TResource? leftResource, TId leftId, ISet<IIdentifiable> rightResourceIds,
69+
CancellationToken cancellationToken)
6970
where TResource : class, IIdentifiable<TId>;
7071

7172
/// <summary>
72-
/// Invokes <see cref="IResourceWriteRepository{TResource,TId}.RemoveFromToManyRelationshipAsync" />.
73+
/// Invokes <see cref="IResourceWriteRepository{TResource,TId}.RemoveFromToManyRelationshipAsync" /> for the specified resource type.
7374
/// </summary>
7475
Task RemoveFromToManyRelationshipAsync<TResource>(TResource leftResource, ISet<IIdentifiable> rightResourceIds, CancellationToken cancellationToken)
7576
where TResource : class, IIdentifiable;

Diff for: src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public interface IResourceWriteRepository<TResource, in TId>
2323
/// <remarks>
2424
/// This method can be overridden to assign resource-specific required relationships.
2525
/// </remarks>
26-
Task<TResource> GetForCreateAsync(TId id, CancellationToken cancellationToken);
26+
Task<TResource> GetForCreateAsync(Type resourceClrType, TId id, CancellationToken cancellationToken);
2727

2828
/// <summary>
2929
/// Creates a new resource in the underlying data store.
@@ -43,7 +43,7 @@ public interface IResourceWriteRepository<TResource, in TId>
4343
/// <summary>
4444
/// Deletes an existing resource from the underlying data store.
4545
/// </summary>
46-
Task DeleteAsync(TId id, CancellationToken cancellationToken);
46+
Task DeleteAsync(TResource? resourceFromDatabase, TId id, CancellationToken cancellationToken);
4747

4848
/// <summary>
4949
/// Performs a complete replacement of the relationship in the underlying data store.
@@ -53,7 +53,7 @@ public interface IResourceWriteRepository<TResource, in TId>
5353
/// <summary>
5454
/// Adds resources to a to-many relationship in the underlying data store.
5555
/// </summary>
56-
Task AddToToManyRelationshipAsync(TId leftId, ISet<IIdentifiable> rightResourceIds, CancellationToken cancellationToken);
56+
Task AddToToManyRelationshipAsync(TResource? leftResource, TId leftId, ISet<IIdentifiable> rightResourceIds, CancellationToken cancellationToken);
5757

5858
/// <summary>
5959
/// Removes resources from a to-many relationship in the underlying data store.

Diff for: src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs

+7-6
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ public async Task<int> CountAsync(ResourceType resourceType, FilterExpression? f
5353
}
5454

5555
/// <inheritdoc />
56-
public async Task<TResource> GetForCreateAsync<TResource, TId>(TId id, CancellationToken cancellationToken)
56+
public async Task<TResource> GetForCreateAsync<TResource, TId>(Type resourceClrType, TId id, CancellationToken cancellationToken)
5757
where TResource : class, IIdentifiable<TId>
5858
{
5959
dynamic repository = GetWriteRepository(typeof(TResource));
60-
return await repository.GetForCreateAsync(id, cancellationToken);
60+
return await repository.GetForCreateAsync(resourceClrType, id, cancellationToken);
6161
}
6262

6363
/// <inheritdoc />
@@ -85,11 +85,11 @@ public async Task UpdateAsync<TResource>(TResource resourceFromRequest, TResourc
8585
}
8686

8787
/// <inheritdoc />
88-
public async Task DeleteAsync<TResource, TId>(TId id, CancellationToken cancellationToken)
88+
public async Task DeleteAsync<TResource, TId>(TResource? resourceFromDatabase, TId id, CancellationToken cancellationToken)
8989
where TResource : class, IIdentifiable<TId>
9090
{
9191
dynamic repository = GetWriteRepository(typeof(TResource));
92-
await repository.DeleteAsync(id, cancellationToken);
92+
await repository.DeleteAsync(resourceFromDatabase, id, cancellationToken);
9393
}
9494

9595
/// <inheritdoc />
@@ -101,11 +101,12 @@ public async Task SetRelationshipAsync<TResource>(TResource leftResource, object
101101
}
102102

103103
/// <inheritdoc />
104-
public async Task AddToToManyRelationshipAsync<TResource, TId>(TId leftId, ISet<IIdentifiable> rightResourceIds, CancellationToken cancellationToken)
104+
public async Task AddToToManyRelationshipAsync<TResource, TId>(TResource? leftResource, TId leftId, ISet<IIdentifiable> rightResourceIds,
105+
CancellationToken cancellationToken)
105106
where TResource : class, IIdentifiable<TId>
106107
{
107108
dynamic repository = GetWriteRepository(typeof(TResource));
108-
await repository.AddToToManyRelationshipAsync(leftId, rightResourceIds, cancellationToken);
109+
await repository.AddToToManyRelationshipAsync(leftResource, leftId, rightResourceIds, cancellationToken);
109110
}
110111

111112
/// <inheritdoc />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace JsonApiDotNetCore.Resources;
2+
3+
/// <inheritdoc cref="IAbstractResourceWrapper" />
4+
internal sealed class AbstractResourceWrapper<TId> : Identifiable<TId>, IAbstractResourceWrapper
5+
{
6+
/// <inheritdoc />
7+
public Type AbstractType { get; }
8+
9+
public AbstractResourceWrapper(Type abstractType)
10+
{
11+
ArgumentGuard.NotNull(abstractType, nameof(abstractType));
12+
13+
AbstractType = abstractType;
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace JsonApiDotNetCore.Resources;
2+
3+
/// <summary>
4+
/// Because an instance cannot be created from an abstract resource type, this wrapper is used to preserve that information.
5+
/// </summary>
6+
internal interface IAbstractResourceWrapper : IIdentifiable
7+
{
8+
/// <summary>
9+
/// The abstract resource type.
10+
/// </summary>
11+
Type AbstractType { get; }
12+
}

0 commit comments

Comments
 (0)