Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 8c3b097

Browse files
author
Bart Koelman
committed
Added ConcurrencyValue to ensure incoming left/right versions are both checked during update
1 parent ff3ac1f commit 8c3b097

38 files changed

+3960
-78
lines changed

‎JsonApiDotNetCore.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,5 +637,6 @@ $left$ = $right$;</s:String>
637637
<s:Boolean x:Key="/Default/UserDictionary/Words/=subdirectory/@EntryIndexedValue">True</s:Boolean>
638638
<s:Boolean x:Key="/Default/UserDictionary/Words/=unarchive/@EntryIndexedValue">True</s:Boolean>
639639
<s:Boolean x:Key="/Default/UserDictionary/Words/=Workflows/@EntryIndexedValue">True</s:Boolean>
640+
<s:Boolean x:Key="/Default/UserDictionary/Words/=xmin/@EntryIndexedValue">True</s:Boolean>
640641
<s:Boolean x:Key="/Default/UserDictionary/Words/=xunit/@EntryIndexedValue">True</s:Boolean>
641642
</wpf:ResourceDictionary>

‎src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Collections.Generic;
22
using JetBrains.Annotations;
33
using JsonApiDotNetCore.Configuration;
4+
using JsonApiDotNetCore.Middleware;
45
using JsonApiDotNetCore.Queries;
56
using JsonApiDotNetCore.Repositories;
67
using JsonApiDotNetCore.Resources;
@@ -13,10 +14,10 @@ namespace MultiDbContextExample.Repositories
1314
public sealed class DbContextARepository<TResource> : EntityFrameworkCoreRepository<TResource, int>
1415
where TResource : class, IIdentifiable<int>
1516
{
16-
public DbContextARepository(ITargetedFields targetedFields, DbContextResolver<DbContextA> dbContextResolver,IResourceGraphresourceGraph,
17-
IResourceFactoryresourceFactory,IEnumerable<IQueryConstraintProvider>constraintProviders,ILoggerFactoryloggerFactory,
18-
IResourceDefinitionAccessorresourceDefinitionAccessor)
19-
: base(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders,loggerFactory,resourceDefinitionAccessor)
17+
public DbContextARepository(IJsonApiRequestrequest,ITargetedFields targetedFields, DbContextResolver<DbContextA> dbContextResolver,
18+
IResourceGraphresourceGraph,IResourceFactoryresourceFactory,IResourceDefinitionAccessorresourceDefinitionAccessor,
19+
IEnumerable<IQueryConstraintProvider>constraintProviders,ILoggerFactoryloggerFactory)
20+
: base(request,targetedFields, dbContextResolver, resourceGraph, resourceFactory, resourceDefinitionAccessor,constraintProviders,loggerFactory)
2021
{
2122
}
2223
}

‎src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Collections.Generic;
22
using JetBrains.Annotations;
33
using JsonApiDotNetCore.Configuration;
4+
using JsonApiDotNetCore.Middleware;
45
using JsonApiDotNetCore.Queries;
56
using JsonApiDotNetCore.Repositories;
67
using JsonApiDotNetCore.Resources;
@@ -13,10 +14,10 @@ namespace MultiDbContextExample.Repositories
1314
public sealed class DbContextBRepository<TResource> : EntityFrameworkCoreRepository<TResource, int>
1415
where TResource : class, IIdentifiable<int>
1516
{
16-
public DbContextBRepository(ITargetedFields targetedFields, DbContextResolver<DbContextB> dbContextResolver,IResourceGraphresourceGraph,
17-
IResourceFactoryresourceFactory,IEnumerable<IQueryConstraintProvider>constraintProviders,ILoggerFactoryloggerFactory,
18-
IResourceDefinitionAccessorresourceDefinitionAccessor)
19-
: base(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders,loggerFactory,resourceDefinitionAccessor)
17+
public DbContextBRepository(IJsonApiRequestrequest,ITargetedFields targetedFields, DbContextResolver<DbContextB> dbContextResolver,
18+
IResourceGraphresourceGraph,IResourceFactoryresourceFactory,IResourceDefinitionAccessorresourceDefinitionAccessor,
19+
IEnumerable<IQueryConstraintProvider>constraintProviders,ILoggerFactoryloggerFactory)
20+
: base(request,targetedFields, dbContextResolver, resourceGraph, resourceFactory, resourceDefinitionAccessor,constraintProviders,loggerFactory)
2021
{
2122
}
2223
}

‎src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ private ICollection<PropertySelector> ToPropertySelectors(IDictionary<ResourceFi
100100

101101
IncludeFieldSelection(resourceFieldSelectors, propertySelectors);
102102

103+
// Implicitly add concurrency tokens, which we need for rendering links, but may not be exposed as attributes.
104+
IncludeConcurrencyTokens(resourceType, elementType, propertySelectors);
105+
103106
IncludeEagerLoads(resourceType, propertySelectors);
104107

105108
return propertySelectors.Values;
@@ -127,6 +130,21 @@ private static void IncludeFieldSelection(IDictionary<ResourceFieldAttribute, Qu
127130
}
128131
}
129132

133+
private void IncludeConcurrencyTokens(ResourceType resourceType, Type elementType, Dictionary<PropertyInfo, PropertySelector> propertySelectors)
134+
{
135+
if (resourceType.IsVersioned)
136+
{
137+
IEntityType entityModel = _entityModel.GetEntityTypes().Single(type => type.ClrType == elementType);
138+
IEnumerable<IProperty> tokenProperties = entityModel.GetProperties().Where(property => property.IsConcurrencyToken).ToArray();
139+
140+
foreach (var tokenProperty in tokenProperties)
141+
{
142+
var propertySelector = new PropertySelector(tokenProperty.PropertyInfo);
143+
IncludeWritableProperty(propertySelector, propertySelectors);
144+
}
145+
}
146+
}
147+
130148
private static void IncludeWritableProperty(PropertySelector propertySelector, Dictionary<PropertyInfo, PropertySelector> propertySelectors)
131149
{
132150
if (propertySelector.Property.SetMethod != null)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
using JetBrains.Annotations;
3+
4+
namespace JsonApiDotNetCore.Repositories
5+
{
6+
/// <summary>
7+
/// The error that is thrown when the resource version from the request does not match the server version.
8+
/// </summary>
9+
[PublicAPI]
10+
public sealed class DataStoreConcurrencyException : DataStoreUpdateException
11+
{
12+
public DataStoreConcurrencyException(Exception? innerException)
13+
: base("The resource version does not match the server version. This indicates that data has been modified since the resource was retrieved.",
14+
innerException)
15+
{
16+
}
17+
}
18+
}

‎src/JsonApiDotNetCore/Repositories/DataStoreUpdateException.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@ namespace JsonApiDotNetCore.Repositories
77
/// The error that is thrown when the underlying data store is unable to persist changes.
88
/// </summary>
99
[PublicAPI]
10-
public sealedclass DataStoreUpdateException : Exception
10+
public class DataStoreUpdateException : Exception
1111
{
1212
public DataStoreUpdateException(Exception? innerException)
13-
: base("Failed to persist changes in the underlying data store.", innerException)
13+
: this("Failed to persist changes in the underlying data store.", innerException)
14+
{
15+
}
16+
17+
protected DataStoreUpdateException(string message, Exception? innerException)
18+
: base(message, innerException)
1419
{
1520
}
1621
}

‎src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public class EntityFrameworkCoreRepository<TResource, TId> : IResourceRepository
3131
where TResource : class, IIdentifiable<TId>
3232
{
3333
private readonly CollectionConverter _collectionConverter = new();
34+
private readonly IJsonApiRequest _request;
3435
private readonly ITargetedFields _targetedFields;
3536
private readonly DbContext _dbContext;
3637
private readonly IResourceGraph _resourceGraph;
@@ -42,24 +43,26 @@ public class EntityFrameworkCoreRepository<TResource, TId> : IResourceRepository
4243
/// <inheritdoc />
4344
public virtual string? TransactionId => _dbContext.Database.CurrentTransaction?.TransactionId.ToString();
4445

45-
public EntityFrameworkCoreRepository(ITargetedFields targetedFields, IDbContextResolver dbContextResolver,IResourceGraphresourceGraph,
46-
IResourceFactoryresourceFactory,IEnumerable<IQueryConstraintProvider>constraintProviders,ILoggerFactoryloggerFactory,
47-
IResourceDefinitionAccessorresourceDefinitionAccessor)
46+
public EntityFrameworkCoreRepository(IJsonApiRequestrequest,ITargetedFields targetedFields, IDbContextResolver dbContextResolver,
47+
IResourceGraphresourceGraph,IResourceFactoryresourceFactory,IResourceDefinitionAccessorresourceDefinitionAccessor,
48+
IEnumerable<IQueryConstraintProvider>constraintProviders,ILoggerFactoryloggerFactory)
4849
{
50+
ArgumentGuard.NotNull(request, nameof(request));
4951
ArgumentGuard.NotNull(targetedFields, nameof(targetedFields));
5052
ArgumentGuard.NotNull(dbContextResolver, nameof(dbContextResolver));
5153
ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph));
5254
ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory));
55+
ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor));
5356
ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders));
5457
ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory));
55-
ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor));
5658

59+
_request = request;
5760
_targetedFields = targetedFields;
5861
_dbContext = dbContextResolver.GetContext();
5962
_resourceGraph = resourceGraph;
6063
_resourceFactory = resourceFactory;
61-
_constraintProviders = constraintProviders;
6264
_resourceDefinitionAccessor = resourceDefinitionAccessor;
65+
_constraintProviders = constraintProviders;
6366
_traceWriter = new TraceLogWriter<EntityFrameworkCoreRepository<TResource, TId>>(loggerFactory);
6467
}
6568

@@ -249,7 +252,11 @@ await _resourceDefinitionAccessor.OnSetToManyRelationshipAsync(leftResource, has
249252
using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Get resource for update");
250253

251254
IReadOnlyCollection<TResource> resources = await GetAsync(queryLayer, cancellationToken);
252-
return resources.FirstOrDefault();
255+
TResource? resource = resources.FirstOrDefault();
256+
257+
resource?.RestoreConcurrencyToken(_dbContext, _request.PrimaryVersion);
258+
259+
return resource;
253260
}
254261

255262
/// <inheritdoc />
@@ -323,6 +330,7 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke
323330
// If so, we'll reuse the tracked resource instead of this placeholder resource.
324331
var placeholderResource = _resourceFactory.CreateInstance<TResource>();
325332
placeholderResource.Id = id;
333+
placeholderResource.RestoreConcurrencyToken(_dbContext, _request.PrimaryVersion);
326334

327335
await _resourceDefinitionAccessor.OnWritingAsync(placeholderResource, WriteOperationKind.DeleteResource, cancellationToken);
328336

@@ -502,6 +510,17 @@ public virtual async Task RemoveFromToManyRelationshipAsync(TResource leftResour
502510

503511
if (!rightResourceIdsToStore.SetEquals(rightResourceIdsStored))
504512
{
513+
if (relationship.RightType.IsVersioned)
514+
{
515+
foreach (IIdentifiable rightResource in rightResourceIdsStored)
516+
{
517+
string? requestVersion = rightResourceIdsToRemove.Single(resource => resource.StringId == rightResource.StringId).GetVersion();
518+
519+
rightResource.RestoreConcurrencyToken(_dbContext, requestVersion);
520+
rightResource.RefreshConcurrencyValue();
521+
}
522+
}
523+
505524
AssertIsNotClearingRequiredToOneRelationship(relationship, leftResourceTracked, rightResourceIdsToStore);
506525

507526
await UpdateRelationshipAsync(relationship, leftResourceTracked, rightResourceIdsToStore, cancellationToken);
@@ -535,6 +554,9 @@ protected async Task UpdateRelationshipAsync(RelationshipAttribute relationship,
535554
await entityEntry.Reference(inversePropertyName).LoadAsync(cancellationToken);
536555
}
537556

557+
leftResource.RestoreConcurrencyToken(_dbContext, _request.PrimaryVersion);
558+
leftResource.RefreshConcurrencyValue();
559+
538560
relationship.SetValue(leftResource, trackedValueToAssign);
539561
}
540562

@@ -548,6 +570,13 @@ protected async Task UpdateRelationshipAsync(RelationshipAttribute relationship,
548570
ICollection<IIdentifiable> rightResources = _collectionConverter.ExtractResources(rightValue);
549571
IIdentifiable[] rightResourcesTracked = rightResources.Select(rightResource => _dbContext.GetTrackedOrAttach(rightResource)).ToArray();
550572

573+
foreach (var rightResourceTracked in rightResourcesTracked)
574+
{
575+
string? rightVersion = rightResourceTracked.GetVersion();
576+
rightResourceTracked.RestoreConcurrencyToken(_dbContext, rightVersion);
577+
rightResourceTracked.RefreshConcurrencyValue();
578+
}
579+
551580
return rightValue is IEnumerable
552581
? _collectionConverter.CopyToTypedCollection(rightResourcesTracked, relationshipPropertyType)
553582
: rightResourcesTracked.Single();
@@ -573,7 +602,7 @@ protected virtual async Task SaveChangesAsync(CancellationToken cancellationToke
573602
{
574603
_dbContext.ResetChangeTracker();
575604

576-
throw new DataStoreUpdateException(exception);
605+
throw exceptionisDbUpdateConcurrencyException?newDataStoreConcurrencyException(exception):new DataStoreUpdateException(exception);
577606
}
578607
}
579608
}

‎src/JsonApiDotNetCore/Resources/IVersionedIdentifiable.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
using System;
2+
using JetBrains.Annotations;
3+
14
namespace JsonApiDotNetCore.Resources
25
{
36
/// <summary>
@@ -21,11 +24,17 @@ public interface IVersionedIdentifiable : IIdentifiable
2124
/// <typeparam name="TVersion">
2225
/// The database vendor-specific type that is used to store the concurrency token.
2326
/// </typeparam>
27+
[PublicAPI]
2428
public interface IVersionedIdentifiable<TId, TVersion> : IIdentifiable<TId>, IVersionedIdentifiable
2529
{
2630
/// <summary>
2731
/// The concurrency token, which is used to detect if the resource was modified by another user since the moment this resource was last retrieved.
2832
/// </summary>
2933
TVersion ConcurrencyToken { get; set; }
34+
35+
/// <summary>
36+
/// Represents a database column where random data is written to on updates, in order to force a concurrency check during relationship updates.
37+
/// </summary>
38+
Guid ConcurrencyValue { get; set; }
3039
}
3140
}

‎src/JsonApiDotNetCore/Resources/IdentifiableExtensions.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
using System;
22
using System.Reflection;
33
using JsonApiDotNetCore.Resources.Internal;
4+
using Microsoft.EntityFrameworkCore;
5+
using Microsoft.EntityFrameworkCore.ChangeTracking;
46

57
namespace JsonApiDotNetCore.Resources
68
{
79
internal static class IdentifiableExtensions
810
{
911
private const string IdPropertyName = nameof(Identifiable<object>.Id);
12+
private const string ConcurrencyTokenPropertyName = nameof(IVersionedIdentifiable<object, object>.ConcurrencyToken);
13+
private const string ConcurrencyValuePropertyName = nameof(IVersionedIdentifiable<object, object>.ConcurrencyValue);
1014

1115
public static object GetTypedId(this IIdentifiable identifiable)
1216
{
@@ -52,5 +56,52 @@ public static void SetVersion(this IIdentifiable identifiable, string? version)
5256
versionedIdentifiable.Version = version;
5357
}
5458
}
59+
60+
public static void RestoreConcurrencyToken(this IIdentifiable identifiable, DbContext dbContext, string? versionFromRequest)
61+
{
62+
ArgumentGuard.NotNull(identifiable, nameof(identifiable));
63+
ArgumentGuard.NotNull(dbContext, nameof(dbContext));
64+
65+
if (identifiable is IVersionedIdentifiable versionedIdentifiable)
66+
{
67+
versionedIdentifiable.Version = versionFromRequest;
68+
69+
PropertyInfo? property = identifiable.GetType().GetProperty(ConcurrencyTokenPropertyName);
70+
71+
if (property == null)
72+
{
73+
throw new InvalidOperationException(
74+
$"Resource of type '{identifiable.GetType()}' does not contain a property named '{ConcurrencyTokenPropertyName}'.");
75+
}
76+
77+
PropertyEntry propertyEntry = dbContext.Entry(identifiable).Property(ConcurrencyTokenPropertyName);
78+
79+
if (!propertyEntry.Metadata.IsConcurrencyToken)
80+
{
81+
throw new InvalidOperationException($"Property '{identifiable.GetType()}.{ConcurrencyTokenPropertyName}' is not a concurrency token.");
82+
}
83+
84+
object? concurrencyTokenFromRequest = property.GetValue(identifiable);
85+
propertyEntry.OriginalValue = concurrencyTokenFromRequest;
86+
}
87+
}
88+
89+
public static void RefreshConcurrencyValue(this IIdentifiable identifiable)
90+
{
91+
ArgumentGuard.NotNull(identifiable, nameof(identifiable));
92+
93+
if (identifiable is IVersionedIdentifiable)
94+
{
95+
PropertyInfo? property = identifiable.GetType().GetProperty(ConcurrencyValuePropertyName);
96+
97+
if (property == null)
98+
{
99+
throw new InvalidOperationException(
100+
$"Resource of type '{identifiable.GetType()}' does not contain a property named '{ConcurrencyValuePropertyName}'.");
101+
}
102+
103+
property.SetValue(identifiable, Guid.NewGuid());
104+
}
105+
}
55106
}
56107
}

‎src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ public sealed class ResourceChangeTracker<TResource> : IResourceChangeTracker<TR
1414
private readonly ResourceType _resourceType;
1515
private readonly ITargetedFields _targetedFields;
1616

17-
private IDictionary<string, string>? _initiallyStoredAttributeValues;
18-
private IDictionary<string, string>? _requestAttributeValues;
19-
private IDictionary<string, string>? _finallyStoredAttributeValues;
17+
private IDictionary<string, string?>? _initiallyStoredAttributeValues;
18+
private IDictionary<string, string?>? _requestAttributeValues;
19+
private IDictionary<string, string?>? _finallyStoredAttributeValues;
2020

2121
public ResourceChangeTracker(IResourceGraph resourceGraph, ITargetedFields targetedFields)
2222
{
@@ -51,9 +51,9 @@ public void SetFinallyStoredAttributeValues(TResource resource)
5151
_finallyStoredAttributeValues = CreateAttributeDictionary(resource, _resourceType.Attributes);
5252
}
5353

54-
private IDictionary<string, string> CreateAttributeDictionary(TResource resource, IEnumerable<AttrAttribute> attributes)
54+
private IDictionary<string, string?> CreateAttributeDictionary(TResource resource, IEnumerable<AttrAttribute> attributes)
5555
{
56-
var result = new Dictionary<string, string>();
56+
var result = new Dictionary<string, string?>();
5757

5858
foreach (AttrAttribute attribute in attributes)
5959
{
@@ -62,6 +62,11 @@ private IDictionary<string, string> CreateAttributeDictionary(TResource resource
6262
result.Add(attribute.PublicName, json);
6363
}
6464

65+
if (resource is IVersionedIdentifiable versionedIdentifiable)
66+
{
67+
result.Add(nameof(versionedIdentifiable.Version), versionedIdentifiable.Version);
68+
}
69+
6570
return result;
6671
}
6772

@@ -74,7 +79,7 @@ public bool HasImplicitChanges()
7479
{
7580
if (_requestAttributeValues.TryGetValue(key, out string? requestValue))
7681
{
77-
string actualValue = _finallyStoredAttributeValues[key];
82+
string? actualValue = _finallyStoredAttributeValues[key];
7883

7984
if (requestValue != actualValue)
8085
{
@@ -83,8 +88,8 @@ public bool HasImplicitChanges()
8388
}
8489
else
8590
{
86-
string initiallyStoredValue = _initiallyStoredAttributeValues[key];
87-
string finallyStoredValue = _finallyStoredAttributeValues[key];
91+
string? initiallyStoredValue = _initiallyStoredAttributeValues[key];
92+
string? finallyStoredValue = _finallyStoredAttributeValues[key];
8893

8994
if (initiallyStoredValue != finallyStoredValue)
9095
{

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /