Skip to content

Commit 3c20861

Browse files
committed
wrap updates in transaction
1 parent 1da949c commit 3c20861

File tree

2 files changed

+96
-9
lines changed

2 files changed

+96
-9
lines changed

src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
using System;
22
using System.Linq;
3+
using System.Threading.Tasks;
34
using JsonApiDotNetCore.Models;
45
using Microsoft.EntityFrameworkCore;
6+
using Microsoft.EntityFrameworkCore.Infrastructure;
7+
using Microsoft.EntityFrameworkCore.Storage;
58

69
namespace JsonApiDotNetCore.Extensions
710
{
@@ -38,5 +41,65 @@ public static bool EntityIsTracked(this DbContext context, IIdentifiable entity)
3841

3942
return trackedEntries != null;
4043
}
44+
45+
/// <summary>
46+
/// Gets the current transaction or creates a new one.
47+
/// If a transaction already exists, commit, rollback and dispose
48+
/// will not be called. It is assumed the creator of the original
49+
/// transaction should be responsible for disposal.
50+
/// </summary>
51+
///
52+
/// <example>
53+
/// <code>
54+
/// using(var transaction = _context.GetCurrentOrCreateTransaction())
55+
/// {
56+
/// // perform multiple operations on the context and then save...
57+
/// _context.SaveChanges();
58+
/// }
59+
/// </code>
60+
/// </example>
61+
public static async Task<IDbContextTransaction> GetCurrentOrCreateTransactionAsync(this DbContext context)
62+
=> await SafeTransactionProxy.GetOrCreateAsync(context.Database);
63+
}
64+
65+
/// <summary>
66+
/// Gets the current transaction or creates a new one.
67+
/// If a transaction already exists, commit, rollback and dispose
68+
/// will not be called. It is assumed the creator of the original
69+
/// transaction should be responsible for disposal.
70+
/// <summary>
71+
internal struct SafeTransactionProxy : IDbContextTransaction
72+
{
73+
private readonly bool _shouldExecute;
74+
private readonly IDbContextTransaction _transaction;
75+
76+
private SafeTransactionProxy(IDbContextTransaction transaction, bool shouldExecute)
77+
{
78+
_transaction = transaction;
79+
_shouldExecute = shouldExecute;
80+
}
81+
82+
public static async Task<IDbContextTransaction> GetOrCreateAsync(DatabaseFacade databaseFacade)
83+
=> (databaseFacade.CurrentTransaction != null)
84+
? new SafeTransactionProxy(databaseFacade.CurrentTransaction, shouldExecute: false)
85+
: new SafeTransactionProxy(await databaseFacade.BeginTransactionAsync(), shouldExecute: true);
86+
87+
/// <inheritdoc />
88+
public Guid TransactionId => _transaction.TransactionId;
89+
90+
/// <inheritdoc />
91+
public void Commit() => Proxy(t => t.Commit());
92+
93+
/// <inheritdoc />
94+
public void Rollback() => Proxy(t => t.Rollback());
95+
96+
/// <inheritdoc />
97+
public void Dispose() => Proxy(t => t.Dispose());
98+
99+
private void Proxy(Action<IDbContextTransaction> func)
100+
{
101+
if(_shouldExecute)
102+
func(_transaction);
103+
}
41104
}
42105
}

src/JsonApiDotNetCore/Internal/Generics/GenericProcessor.cs

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,22 @@
77
using JsonApiDotNetCore.Extensions;
88
using JsonApiDotNetCore.Models;
99
using Microsoft.EntityFrameworkCore;
10+
using Microsoft.EntityFrameworkCore.Infrastructure;
11+
using Microsoft.EntityFrameworkCore.Storage;
1012

1113
namespace JsonApiDotNetCore.Internal.Generics
1214
{
15+
// TODO: consider renaming to PatchRelationshipService (or something)
1316
public interface IGenericProcessor
1417
{
1518
Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds);
16-
void SetRelationships(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds);
1719
}
1820

21+
/// <summary>
22+
/// A special processor that gets instantiated for a generic type (&lt;T&gt;)
23+
/// when the actual type is not known until runtime. Specifically, this is used for updating
24+
/// relationships.
25+
/// </summary>
1926
public class GenericProcessor<T> : IGenericProcessor where T : class
2027
{
2128
private readonly DbContext _context;
@@ -26,14 +33,21 @@ public GenericProcessor(IDbContextResolver contextResolver)
2633

2734
public virtual async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
2835
{
29-
SetRelationships(parent, relationship, relationshipIds);
30-
31-
await _context.SaveChangesAsync();
36+
if (relationship is HasManyThroughAttribute hasManyThrough && parent is IIdentifiable identifiableParent)
37+
{
38+
await SetHasManyThroughRelationshipAsync(identifiableParent, hasManyThrough, relationshipIds);
39+
}
40+
else
41+
{
42+
await SetRelationshipsAsync(parent, relationship, relationshipIds);
43+
}
3244
}
3345

34-
public virtual void SetRelationships(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
46+
private async Task SetHasManyThroughRelationshipAsync(IIdentifiable identifiableParent, HasManyThroughAttribute hasManyThrough, IEnumerable<string> relationshipIds)
3547
{
36-
if (relationship is HasManyThroughAttribute hasManyThrough && parent is IIdentifiable identifiableParent)
48+
// we need to create a transaction for the HasManyThrough case so we can get and remove any existing
49+
// join entities and only commit if all operations are successful
50+
using(var transaction = await _context.GetCurrentOrCreateTransactionAsync())
3751
{
3852
// ArticleTag
3953
ParameterExpression parameter = Expression.Parameter(hasManyThrough.ThroughType);
@@ -50,13 +64,14 @@ public virtual void SetRelationships(object parent, RelationshipAttribute relati
5064

5165
var lambda = Expression.Lambda<Func<T, bool>>(equals, parameter);
5266

67+
// TODO: we shouldn't need to do this instead we should try updating the existing?
68+
// the challenge here is if a composite key is used, then we will fail to
69+
// create due to a unique key violation
5370
var oldLinks = _context
5471
.Set<T>()
5572
.Where(lambda.Compile())
5673
.ToList();
5774

58-
// TODO: we shouldn't need to do this and it especially shouldn't happen outside a transaction
59-
// instead we should try updating the existing?
6075
_context.RemoveRange(oldLinks);
6176

6277
var newLinks = relationshipIds.Select(x => {
@@ -67,8 +82,15 @@ public virtual void SetRelationships(object parent, RelationshipAttribute relati
6782
});
6883

6984
_context.AddRange(newLinks);
85+
await _context.SaveChangesAsync();
86+
87+
transaction.Commit();
7088
}
71-
else if (relationship.IsHasMany)
89+
}
90+
91+
private async Task SetRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
92+
{
93+
if (relationship.IsHasMany)
7294
{
7395
// TODO: need to handle the failure mode when the relationship does not implement IIdentifiable
7496
var entities = _context.Set<T>().Where(x => relationshipIds.Contains(((IIdentifiable)x).StringId)).ToList();
@@ -80,6 +102,8 @@ public virtual void SetRelationships(object parent, RelationshipAttribute relati
80102
var entity = _context.Set<T>().SingleOrDefault(x => relationshipIds.First() == ((IIdentifiable)x).StringId);
81103
relationship.SetValue(parent, entity);
82104
}
105+
106+
await _context.SaveChangesAsync();
83107
}
84108
}
85109
}

0 commit comments

Comments
 (0)