Skip to content

Commit

Permalink
Resource inheritance: write endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
Bart Koelman committed Mar 30, 2022
1 parent 4d34dcd commit 68063dc
Show file tree
Hide file tree
Showing 37 changed files with 3,452 additions and 120 deletions.
4 changes: 2 additions & 2 deletions benchmarks/Serialization/SerializationBenchmarkBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,9 @@ public Task OnSetToManyRelationshipAsync<TResource>(TResource leftResource, HasM
return Task.CompletedTask;
}

public Task OnAddToRelationshipAsync<TResource, TId>(TId leftResourceId, HasManyAttribute hasManyRelationship, ISet<IIdentifiable> rightResourceIds,
public Task OnAddToRelationshipAsync<TResource>(TResource leftResource, HasManyAttribute hasManyRelationship, ISet<IIdentifiable> rightResourceIds,
CancellationToken cancellationToken)
where TResource : class, IIdentifiable<TId>
where TResource : class, IIdentifiable
{
return Task.CompletedTask;
}
Expand Down
42 changes: 42 additions & 0 deletions src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,43 @@ public IReadOnlySet<ResourceType> GetAllConcreteDerivedTypes()
return _lazyAllConcreteDerivedTypes.Value;
}

/// <summary>
/// Searches the tree of derived types to find a match for the specified <paramref name="clrType" />.
/// </summary>
public ResourceType GetTypeOrDerived(Type clrType)
{
ArgumentGuard.NotNull(clrType, nameof(clrType));

ResourceType? derivedType = FindTypeOrDerived(this, clrType);

if (derivedType == null)
{
throw new InvalidOperationException($"Resource type '{PublicName}' is not a base type of '{clrType}'.");
}

return derivedType;
}

private static ResourceType? FindTypeOrDerived(ResourceType type, Type clrType)
{
if (type.ClrType == clrType)
{
return type;
}

foreach (ResourceType derivedType in type.DirectlyDerivedTypes)
{
ResourceType? matchingType = FindTypeOrDerived(derivedType, clrType);

if (matchingType != null)
{
return matchingType;
}
}

return null;
}

internal IReadOnlySet<AttrAttribute> GetAttributesInTypeOrDerived(string publicName)
{
return GetAttributesInTypeOrDerived(this, publicName);
Expand Down Expand Up @@ -261,6 +298,11 @@ private static IReadOnlySet<RelationshipAttribute> GetRelationshipsInTypeOrDeriv
return relationshipsInDerivedTypes;
}

internal bool IsPartOfTypeHierarchy()
{
return BaseType != null || DirectlyDerivedTypes.Any();
}

public override string ToString()
{
return PublicName;
Expand Down
4 changes: 2 additions & 2 deletions src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ public QueryLayer ComposeForGetRelationshipRightIds(RelationshipAttribute relati

AttrAttribute rightIdAttribute = GetIdAttribute(relationship.RightType);

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

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

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

FilterExpression? leftFilter = CreateFilterByIds(leftId.AsArray(), leftIdAttribute, null);
FilterExpression? rightFilter = CreateFilterByIds(rightTypedIds, rightIdAttribute, null);
Expand Down
58 changes: 45 additions & 13 deletions src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,15 @@ protected virtual IQueryable<TResource> GetAll()
}

/// <inheritdoc />
public virtual Task<TResource> GetForCreateAsync(TId id, CancellationToken cancellationToken)
public virtual Task<TResource> GetForCreateAsync(Type resourceClrType, TId id, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
resourceClrType,
id
});

var resource = _resourceFactory.CreateInstance<TResource>();
var resource = (TResource)_resourceFactory.CreateInstance(resourceClrType);
resource.Id = id;

return Task.FromResult(resource);
Expand Down Expand Up @@ -305,18 +306,19 @@ protected void AssertIsNotClearingRequiredToOneRelationship(RelationshipAttribut
}

/// <inheritdoc />
public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToken)
public virtual async Task DeleteAsync(TResource? resourceFromDatabase, TId id, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
resourceFromDatabase,
id
});

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

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

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

/// <inheritdoc />
public virtual async Task AddToToManyRelationshipAsync(TId leftId, ISet<IIdentifiable> rightResourceIds, CancellationToken cancellationToken)
public virtual async Task AddToToManyRelationshipAsync(TResource? leftResource, TId leftId, ISet<IIdentifiable> rightResourceIds,
CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
leftResource,
leftId,
rightResourceIds
});
Expand All @@ -427,25 +431,53 @@ public virtual async Task AddToToManyRelationshipAsync(TId leftId, ISet<IIdentif

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

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

await _resourceDefinitionAccessor.OnAddToRelationshipAsync(leftPlaceholderResource, relationship, rightResourceIds, cancellationToken);

if (rightResourceIds.Any())
{
var leftPlaceholderResource = _resourceFactory.CreateInstance<TResource>();
leftPlaceholderResource.Id = leftId;

var leftResourceTracked = (TResource)_dbContext.GetTrackedOrAttach(leftPlaceholderResource);
IEnumerable rightValueToStore = GetRightValueToStoreForAddToToMany(leftResourceTracked, relationship, rightResourceIds);

await UpdateRelationshipAsync(relationship, leftResourceTracked, rightResourceIds, cancellationToken);
await UpdateRelationshipAsync(relationship, leftResourceTracked, rightValueToStore, cancellationToken);

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

await SaveChangesAsync(cancellationToken);

await _resourceDefinitionAccessor.OnWriteSucceededAsync(leftResourceTracked, WriteOperationKind.AddToRelationship, cancellationToken);
}
}

private IEnumerable GetRightValueToStoreForAddToToMany(TResource leftResource, HasManyAttribute relationship, ISet<IIdentifiable> rightResourceIdsToAdd)
{
object? rightValueStored = relationship.GetValue(leftResource);

// @formatter:wrap_chained_method_calls chop_always
// @formatter:keep_existing_linebreaks true

HashSet<IIdentifiable> rightResourceIdsStored = _collectionConverter
.ExtractResources(rightValueStored)
.Select(rightResource => _dbContext.GetTrackedOrAttach(rightResource))
.ToHashSet(IdentifiableComparer.Instance);

// @formatter:keep_existing_linebreaks restore
// @formatter:wrap_chained_method_calls restore

if (rightResourceIdsStored.Any())
{
rightResourceIdsStored.AddRange(rightResourceIdsToAdd);
return rightResourceIdsStored;
}

return rightResourceIdsToAdd;
}

/// <inheritdoc />
public virtual async Task RemoveFromToManyRelationshipAsync(TResource leftResource, ISet<IIdentifiable> rightResourceIds,
CancellationToken cancellationToken)
Expand Down Expand Up @@ -473,7 +505,7 @@ public virtual async Task RemoveFromToManyRelationshipAsync(TResource leftResour
// Make Entity Framework Core believe any additional resources added from ResourceDefinition already exist in database.
IIdentifiable[] extraResourceIdsToRemove = rightResourceIdsToRemove.Where(rightId => !rightResourceIds.Contains(rightId)).ToArray();

object? rightValueStored = relationship.GetValue(leftResource);
object? rightValueStored = relationship.GetValue(leftResourceTracked);

// @formatter:wrap_chained_method_calls chop_always
// @formatter:keep_existing_linebreaks true
Expand All @@ -488,9 +520,9 @@ public virtual async Task RemoveFromToManyRelationshipAsync(TResource leftResour
// @formatter:wrap_chained_method_calls restore

rightValueStored = _collectionConverter.CopyToTypedCollection(rightResourceIdsStored, relationship.Property.PropertyType);
relationship.SetValue(leftResource, rightValueStored);
relationship.SetValue(leftResourceTracked, rightValueStored);

MarkRelationshipAsLoaded(leftResource, relationship);
MarkRelationshipAsLoaded(leftResourceTracked, relationship);

HashSet<IIdentifiable> rightResourceIdsToStore = rightResourceIdsStored.ToHashSet(IdentifiableComparer.Instance);
rightResourceIdsToStore.ExceptWith(rightResourceIdsToRemove);
Expand Down
21 changes: 11 additions & 10 deletions src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace JsonApiDotNetCore.Repositories;
public interface IResourceRepositoryAccessor
{
/// <summary>
/// Invokes <see cref="IResourceReadRepository{TResource,TId}.GetAsync" />.
/// Invokes <see cref="IResourceReadRepository{TResource,TId}.GetAsync" /> for the specified resource type.
/// </summary>
Task<IReadOnlyCollection<TResource>> GetAsync<TResource>(QueryLayer queryLayer, CancellationToken cancellationToken)
where TResource : class, IIdentifiable;
Expand All @@ -27,49 +27,50 @@ Task<IReadOnlyCollection<TResource>> GetAsync<TResource>(QueryLayer queryLayer,
Task<int> CountAsync(ResourceType resourceType, FilterExpression? filter, CancellationToken cancellationToken);

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

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

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

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

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

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

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

/// <summary>
/// Invokes <see cref="IResourceWriteRepository{TResource,TId}.RemoveFromToManyRelationshipAsync" />.
/// Invokes <see cref="IResourceWriteRepository{TResource,TId}.RemoveFromToManyRelationshipAsync" /> for the specified resource type.
/// </summary>
Task RemoveFromToManyRelationshipAsync<TResource>(TResource leftResource, ISet<IIdentifiable> rightResourceIds, CancellationToken cancellationToken)
where TResource : class, IIdentifiable;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public interface IResourceWriteRepository<TResource, in TId>
/// <remarks>
/// This method can be overridden to assign resource-specific required relationships.
/// </remarks>
Task<TResource> GetForCreateAsync(TId id, CancellationToken cancellationToken);
Task<TResource> GetForCreateAsync(Type resourceClrType, TId id, CancellationToken cancellationToken);

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

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

/// <summary>
/// Removes resources from a to-many relationship in the underlying data store.
Expand Down
13 changes: 7 additions & 6 deletions src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ public async Task<int> CountAsync(ResourceType resourceType, FilterExpression? f
}

/// <inheritdoc />
public async Task<TResource> GetForCreateAsync<TResource, TId>(TId id, CancellationToken cancellationToken)
public async Task<TResource> GetForCreateAsync<TResource, TId>(Type resourceClrType, TId id, CancellationToken cancellationToken)
where TResource : class, IIdentifiable<TId>
{
dynamic repository = GetWriteRepository(typeof(TResource));
return await repository.GetForCreateAsync(id, cancellationToken);
return await repository.GetForCreateAsync(resourceClrType, id, cancellationToken);
}

/// <inheritdoc />
Expand Down Expand Up @@ -85,11 +85,11 @@ public async Task UpdateAsync<TResource>(TResource resourceFromRequest, TResourc
}

/// <inheritdoc />
public async Task DeleteAsync<TResource, TId>(TId id, CancellationToken cancellationToken)
public async Task DeleteAsync<TResource, TId>(TResource? resourceFromDatabase, TId id, CancellationToken cancellationToken)
where TResource : class, IIdentifiable<TId>
{
dynamic repository = GetWriteRepository(typeof(TResource));
await repository.DeleteAsync(id, cancellationToken);
await repository.DeleteAsync(resourceFromDatabase, id, cancellationToken);
}

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

/// <inheritdoc />
public async Task AddToToManyRelationshipAsync<TResource, TId>(TId leftId, ISet<IIdentifiable> rightResourceIds, CancellationToken cancellationToken)
public async Task AddToToManyRelationshipAsync<TResource, TId>(TResource? leftResource, TId leftId, ISet<IIdentifiable> rightResourceIds,
CancellationToken cancellationToken)
where TResource : class, IIdentifiable<TId>
{
dynamic repository = GetWriteRepository(typeof(TResource));
await repository.AddToToManyRelationshipAsync(leftId, rightResourceIds, cancellationToken);
await repository.AddToToManyRelationshipAsync(leftResource, leftId, rightResourceIds, cancellationToken);
}

/// <inheritdoc />
Expand Down
15 changes: 15 additions & 0 deletions src/JsonApiDotNetCore/Resources/AbstractResourceWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace JsonApiDotNetCore.Resources;

/// <inheritdoc cref="IAbstractResourceWrapper" />
internal sealed class AbstractResourceWrapper<TId> : Identifiable<TId>, IAbstractResourceWrapper
{
/// <inheritdoc />
public Type AbstractType { get; }

public AbstractResourceWrapper(Type abstractType)
{
ArgumentGuard.NotNull(abstractType, nameof(abstractType));

AbstractType = abstractType;
}
}
12 changes: 12 additions & 0 deletions src/JsonApiDotNetCore/Resources/IAbstractResourceWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace JsonApiDotNetCore.Resources;

/// <summary>
/// Because an instance cannot be created from an abstract resource type, this wrapper is used to preserve that information.
/// </summary>
internal interface IAbstractResourceWrapper : IIdentifiable
{
/// <summary>
/// The abstract resource type.
/// </summary>
Type AbstractType { get; }
}
Loading

0 comments on commit 68063dc

Please sign in to comment.