Skip to content

Write callbacks #977

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
May 19, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions JsonApiDotNetCore.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -621,9 +621,12 @@ $left$ = $right$;</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Assignee/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Injectables/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=linebreaks/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Microservices/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=navigations/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=playlists/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Rewriter/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Startups/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=subdirectory/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unarchive/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Workflows/@EntryIndexedValue">True</s:Boolean>
</wpf:ResourceDictionary>
6 changes: 4 additions & 2 deletions benchmarks/Serialization/JsonApiSerializerBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ public JsonApiSerializerBenchmarks()

var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, new ResourceObjectBuilderSettings());

_jsonApiSerializer = new ResponseSerializer<BenchmarkResource>(metaBuilder, linkBuilder,
includeBuilder, fieldsToSerialize, resourceObjectBuilder, options);
IResourceDefinitionAccessor resourceDefinitionAccessor = new Mock<IResourceDefinitionAccessor>().Object;

_jsonApiSerializer = new ResponseSerializer<BenchmarkResource>(metaBuilder, linkBuilder, includeBuilder, fieldsToSerialize, resourceObjectBuilder,
resourceDefinitionAccessor, options);
}

private static FieldsToSerialize CreateFieldsToSerialize(IResourceGraph resourceGraph)
Expand Down
14 changes: 14 additions & 0 deletions src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using JetBrains.Annotations;
using JsonApiDotNetCore.Errors;
using JsonApiDotNetCore.Repositories;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Serialization.Building;
using JsonApiDotNetCore.Serialization.Client.Internal;
using JsonApiDotNetCore.Services;
Expand Down Expand Up @@ -101,6 +102,19 @@ public static IServiceCollection AddResourceRepository<TRepository>(this IServic
return services;
}

/// <summary>
/// Adds IoC container registrations for the various JsonApiDotNetCore resource definition interfaces, such as
/// <see cref="IResourceDefinition{TResource}" /> and <see cref="IResourceDefinition{TResource,TId}" />.
/// </summary>
public static IServiceCollection AddResourceDefinition<TResourceDefinition>(this IServiceCollection services)
{
ArgumentGuard.NotNull(services, nameof(services));

RegisterForConstructedType(services, typeof(TResourceDefinition), ServiceDiscoveryFacade.ResourceDefinitionInterfaces);

return services;
}

private static void RegisterForConstructedType(IServiceCollection services, Type implementationType, IEnumerable<Type> openGenericInterfaces)
{
bool seenCompatibleInterface = false;
Expand Down
26 changes: 25 additions & 1 deletion src/JsonApiDotNetCore/Middleware/OperationKind.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
namespace JsonApiDotNetCore.Middleware
{
/// <summary>
/// Lists the functional operation kinds from an atomic:operations request.
/// Lists the functional operation kinds from a resource request or an atomic:operations request.
/// </summary>
public enum OperationKind
{
/// <summary>
/// Create a new resource with attributes, relationships or both.
/// </summary>
CreateResource,

/// <summary>
/// Update the attributes and/or relationships of an existing resource. Only the values of sent attributes are replaced. And only the values of sent
/// relationships are replaced.
/// </summary>
UpdateResource,

/// <summary>
/// Delete an existing resource.
/// </summary>
DeleteResource,

/// <summary>
/// Perform a complete replacement of a relationship on an existing resource.
/// </summary>
SetRelationship,

/// <summary>
/// Add resources to a to-many relationship.
/// </summary>
AddToRelationship,

/// <summary>
/// Remove resources from a to-many relationship.
/// </summary>
RemoveFromRelationship
}
}
9 changes: 9 additions & 0 deletions src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Queries.Expressions;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Services;

namespace JsonApiDotNetCore.Queries
{
Expand Down Expand Up @@ -57,5 +59,12 @@ QueryLayer WrapLayerForSecondaryEndpoint<TId>(QueryLayer secondaryLayer, Resourc
/// Builds a query for a to-many relationship with a filter to match on its left and right resource IDs.
/// </summary>
QueryLayer ComposeForHasMany<TId>(HasManyAttribute hasManyRelationship, TId leftId, ICollection<IIdentifiable> rightResourceIds);

/// <summary>
/// Provides access to the request-scoped <see cref="IResourceDefinitionAccessor" /> instance. This method has been added solely to prevent introducing a
/// breaking change in the <see cref="JsonApiResourceService{TResource,TId}" /> constructor and will be removed in the next major version.
/// </summary>
[Obsolete]
IResourceDefinitionAccessor GetResourceDefinitionAccessor();
}
}
6 changes: 6 additions & 0 deletions src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,12 @@ public QueryLayer ComposeForHasMany<TId>(HasManyAttribute hasManyRelationship, T
};
}

/// <inheritdoc />
public IResourceDefinitionAccessor GetResourceDefinitionAccessor()
{
return _resourceDefinitionAccessor;
}

protected virtual IReadOnlyCollection<IncludeElementExpression> GetIncludeElements(IReadOnlyCollection<IncludeElementExpression> includeElements,
ResourceContext resourceContext)
{
Expand Down
111 changes: 93 additions & 18 deletions src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public class EntityFrameworkCoreRepository<TResource, TId> : IResourceRepository
private readonly IResourceFactory _resourceFactory;
private readonly IEnumerable<IQueryConstraintProvider> _constraintProviders;
private readonly TraceLogWriter<EntityFrameworkCoreRepository<TResource, TId>> _traceWriter;
private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor;

/// <inheritdoc />
public virtual string TransactionId => _dbContext.Database.CurrentTransaction?.TransactionId.ToString();
Expand All @@ -55,6 +56,10 @@ public EntityFrameworkCoreRepository(ITargetedFields targetedFields, IDbContextR
_constraintProviders = constraintProviders;
_dbContext = contextResolver.GetContext();
_traceWriter = new TraceLogWriter<EntityFrameworkCoreRepository<TResource, TId>>(loggerFactory);

#pragma warning disable 612 // Method is obsolete
_resourceDefinitionAccessor = resourceFactory.GetResourceDefinitionAccessor();
#pragma warning restore 612
}

/// <inheritdoc />
Expand Down Expand Up @@ -167,18 +172,48 @@ public virtual async Task CreateAsync(TResource resourceFromRequest, TResource r
foreach (RelationshipAttribute relationship in _targetedFields.Relationships)
{
object rightResources = relationship.GetValue(resourceFromRequest);
await UpdateRelationshipAsync(relationship, resourceForDatabase, rightResources, collector, cancellationToken);

object rightResourcesEdited = await EditRelationshipAsync(resourceForDatabase, relationship, rightResources, OperationKind.CreateResource,
cancellationToken);

await UpdateRelationshipAsync(relationship, resourceForDatabase, rightResourcesEdited, collector, cancellationToken);
}

foreach (AttrAttribute attribute in _targetedFields.Attributes)
{
attribute.SetValue(resourceForDatabase, attribute.GetValue(resourceFromRequest));
}

await _resourceDefinitionAccessor.OnWritingAsync(resourceForDatabase, OperationKind.CreateResource, cancellationToken);

DbSet<TResource> dbSet = _dbContext.Set<TResource>();
await dbSet.AddAsync(resourceForDatabase, cancellationToken);

await SaveChangesAsync(cancellationToken);

await _resourceDefinitionAccessor.OnWriteSucceededAsync(resourceForDatabase, OperationKind.CreateResource, cancellationToken);
}

private async Task<object> EditRelationshipAsync(TResource leftResource, RelationshipAttribute relationship, object rightResourceIds,
OperationKind operationKind, CancellationToken cancellationToken)
{
if (relationship is HasOneAttribute hasOneRelationship)
{
return await _resourceDefinitionAccessor.OnSetToOneRelationshipAsync(leftResource, hasOneRelationship, (IIdentifiable)rightResourceIds,
operationKind, cancellationToken);
}

if (relationship is HasManyAttribute hasManyRelationship)
{
HashSet<IIdentifiable> rightResourceIdSet = _collectionConverter.ExtractResources(rightResourceIds).ToHashSet(IdentifiableComparer.Instance);

await _resourceDefinitionAccessor.OnSetToManyRelationshipAsync(leftResource, hasManyRelationship, rightResourceIdSet, operationKind,
cancellationToken);

return rightResourceIdSet;
}

return rightResourceIds;
}

/// <inheritdoc />
Expand Down Expand Up @@ -206,17 +241,24 @@ public virtual async Task UpdateAsync(TResource resourceFromRequest, TResource r
{
object rightResources = relationship.GetValue(resourceFromRequest);

AssertIsNotClearingRequiredRelationship(relationship, resourceFromDatabase, rightResources);
object rightResourcesEdited = await EditRelationshipAsync(resourceFromDatabase, relationship, rightResources, OperationKind.UpdateResource,
cancellationToken);

AssertIsNotClearingRequiredRelationship(relationship, resourceFromDatabase, rightResourcesEdited);

await UpdateRelationshipAsync(relationship, resourceFromDatabase, rightResources, collector, cancellationToken);
await UpdateRelationshipAsync(relationship, resourceFromDatabase, rightResourcesEdited, collector, cancellationToken);
}

foreach (AttrAttribute attribute in _targetedFields.Attributes)
{
attribute.SetValue(resourceFromDatabase, attribute.GetValue(resourceFromRequest));
}

await _resourceDefinitionAccessor.OnWritingAsync(resourceFromDatabase, OperationKind.UpdateResource, cancellationToken);

await SaveChangesAsync(cancellationToken);

await _resourceDefinitionAccessor.OnWriteSucceededAsync(resourceFromDatabase, OperationKind.UpdateResource, cancellationToken);
}

protected void AssertIsNotClearingRequiredRelationship(RelationshipAttribute relationship, TResource leftResource, object rightValue)
Expand All @@ -229,9 +271,9 @@ protected void AssertIsNotClearingRequiredRelationship(RelationshipAttribute rel
relationshipIsRequired = navigation?.ForeignKey?.IsRequired ?? false;
}

bool relationshipIsBeingCleared = relationship is HasOneAttribute
? rightValue == null
: IsToManyRelationshipBeingCleared(relationship, leftResource, rightValue);
bool relationshipIsBeingCleared = relationship is HasManyAttribute hasManyRelationship
? IsToManyRelationshipBeingCleared(hasManyRelationship, leftResource, rightValue)
: rightValue == null;

if (relationshipIsRequired && relationshipIsBeingCleared)
{
Expand All @@ -240,11 +282,11 @@ protected void AssertIsNotClearingRequiredRelationship(RelationshipAttribute rel
}
}

private bool IsToManyRelationshipBeingCleared(RelationshipAttribute relationship, TResource leftResource, object valueToAssign)
private bool IsToManyRelationshipBeingCleared(HasManyAttribute hasManyRelationship, TResource leftResource, object valueToAssign)
{
ICollection<IIdentifiable> newRightResourceIds = _collectionConverter.ExtractResources(valueToAssign);

object existingRightValue = relationship.GetValue(leftResource);
object existingRightValue = hasManyRelationship.GetValue(leftResource);

HashSet<IIdentifiable> existingRightResourceIds =
_collectionConverter.ExtractResources(existingRightValue).ToHashSet(IdentifiableComparer.Instance);
Expand All @@ -262,6 +304,13 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke
id
});

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

await _resourceDefinitionAccessor.OnWritingAsync(emptyResource, OperationKind.DeleteResource, cancellationToken);

using var collector = new PlaceholderResourceCollector(_resourceFactory, _dbContext);
TResource resource = collector.CreateForId<TResource, TId>(id);

Expand All @@ -279,6 +328,8 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke
_dbContext.Remove(resource);

await SaveChangesAsync(cancellationToken);

await _resourceDefinitionAccessor.OnWriteSucceededAsync(resource, OperationKind.DeleteResource, cancellationToken);
}

private NavigationEntry GetNavigationEntry(TResource resource, RelationshipAttribute relationship)
Expand Down Expand Up @@ -340,12 +391,19 @@ public virtual async Task SetRelationshipAsync(TResource primaryResource, object

RelationshipAttribute relationship = _targetedFields.Relationships.Single();

AssertIsNotClearingRequiredRelationship(relationship, primaryResource, secondaryResourceIds);
object secondaryResourceIdsEdited =
await EditRelationshipAsync(primaryResource, relationship, secondaryResourceIds, OperationKind.SetRelationship, cancellationToken);

AssertIsNotClearingRequiredRelationship(relationship, primaryResource, secondaryResourceIdsEdited);

using var collector = new PlaceholderResourceCollector(_resourceFactory, _dbContext);
await UpdateRelationshipAsync(relationship, primaryResource, secondaryResourceIds, collector, cancellationToken);
await UpdateRelationshipAsync(relationship, primaryResource, secondaryResourceIdsEdited, collector, cancellationToken);

await _resourceDefinitionAccessor.OnWritingAsync(primaryResource, OperationKind.SetRelationship, cancellationToken);

await SaveChangesAsync(cancellationToken);

await _resourceDefinitionAccessor.OnWriteSucceededAsync(primaryResource, OperationKind.SetRelationship, cancellationToken);
}

/// <inheritdoc />
Expand All @@ -359,7 +417,9 @@ public virtual async Task AddToToManyRelationshipAsync(TId primaryId, ISet<IIden

ArgumentGuard.NotNull(secondaryResourceIds, nameof(secondaryResourceIds));

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

await _resourceDefinitionAccessor.OnAddToRelationshipAsync<TResource, TId>(primaryId, relationship, secondaryResourceIds, cancellationToken);

if (secondaryResourceIds.Any())
{
Expand All @@ -368,7 +428,11 @@ public virtual async Task AddToToManyRelationshipAsync(TId primaryId, ISet<IIden

await UpdateRelationshipAsync(relationship, primaryResource, secondaryResourceIds, collector, cancellationToken);

await _resourceDefinitionAccessor.OnWritingAsync(primaryResource, OperationKind.AddToRelationship, cancellationToken);

await SaveChangesAsync(cancellationToken);

await _resourceDefinitionAccessor.OnWriteSucceededAsync(primaryResource, OperationKind.AddToRelationship, cancellationToken);
}
}

Expand All @@ -386,17 +450,26 @@ public virtual async Task RemoveFromToManyRelationshipAsync(TResource primaryRes

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

object rightValue = relationship.GetValue(primaryResource);
await _resourceDefinitionAccessor.OnRemoveFromRelationshipAsync(primaryResource, relationship, secondaryResourceIds, cancellationToken);

HashSet<IIdentifiable> rightResourceIds = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance);
rightResourceIds.ExceptWith(secondaryResourceIds);
if (secondaryResourceIds.Any())
{
object rightValue = relationship.GetValue(primaryResource);

AssertIsNotClearingRequiredRelationship(relationship, primaryResource, rightResourceIds);
HashSet<IIdentifiable> rightResourceIds = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance);
rightResourceIds.ExceptWith(secondaryResourceIds);

using var collector = new PlaceholderResourceCollector(_resourceFactory, _dbContext);
await UpdateRelationshipAsync(relationship, primaryResource, rightResourceIds, collector, cancellationToken);
AssertIsNotClearingRequiredRelationship(relationship, primaryResource, rightResourceIds);

await SaveChangesAsync(cancellationToken);
using var collector = new PlaceholderResourceCollector(_resourceFactory, _dbContext);
await UpdateRelationshipAsync(relationship, primaryResource, rightResourceIds, collector, cancellationToken);

await _resourceDefinitionAccessor.OnWritingAsync(primaryResource, OperationKind.RemoveFromRelationship, cancellationToken);

await SaveChangesAsync(cancellationToken);

await _resourceDefinitionAccessor.OnWriteSucceededAsync(primaryResource, OperationKind.RemoveFromRelationship, cancellationToken);
}
}

protected async Task UpdateRelationshipAsync(RelationshipAttribute relationship, TResource leftResource, object valueToAssign,
Expand Down Expand Up @@ -449,6 +522,8 @@ private bool IsOneToOneRelationship(RelationshipAttribute relationship)

protected virtual async Task SaveChangesAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

try
{
await _dbContext.SaveChangesAsync(cancellationToken);
Expand Down
Loading