Skip to content

Commit e4925d8

Browse files
committed
disposed DbContext bug fixed
1 parent 05c6ae7 commit e4925d8

File tree

6 files changed

+92
-74
lines changed

6 files changed

+92
-74
lines changed

src/SampleDotnet.RepositoryFactory/Extensions/DbContextExtensions.cs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,4 @@ internal static async Task RollbackChangesAsync(this DbContext context, bool ove
4545
}
4646
}
4747
}
48-
49-
internal static void SilentDbContextDispose(this DbContext dbContext, bool acceptAllChangesBeforeDisposing = false)
50-
{
51-
if (acceptAllChangesBeforeDisposing)
52-
dbContext.ChangeTracker.AcceptAllChanges();
53-
54-
try
55-
{
56-
dbContext.Dispose();
57-
}
58-
catch (Exception e)
59-
{
60-
}
61-
}
6248
}

src/SampleDotnet.RepositoryFactory/Interfaces/IRepository.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
public interface IRepository : IDisposable
44
{
5-
DbContextId CurrentContextId => CurrentDbContext.ContextId;
5+
DbContextId CurrentContextId => DbContext.ContextId;
66

7-
DatabaseFacade Database => CurrentDbContext.Database;
7+
DatabaseFacade Database => DbContext.Database;
88

9-
Type DbContextType => CurrentDbContext.GetType();
9+
ChangeTracker ChangeTracker => DbContext.ChangeTracker;
1010

11-
protected abstract DbContext CurrentDbContext { get; }
11+
DbContext DbContext { get; }
1212

1313
void Delete(object entity);
1414

@@ -17,6 +17,8 @@ public interface IRepository : IDisposable
1717
void Update(object entity);
1818

1919
void UpdateRange(params object[] entities);
20+
21+
DbContext RefreshDbContext();
2022
}
2123

2224
public interface IRepository<TDbContext> : IRepository

src/SampleDotnet.RepositoryFactory/Repository.cs

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,25 @@ internal class Repository<TDbContext> : RepositoryBase
44
, IRepository<TDbContext>
55
where TDbContext : DbContext
66
{
7-
private static readonly Func<object, object> funcCreatedAt = new((entity) =>
7+
private static readonly Func<object, object> _funcCreatedAt = new((entity) =>
88
{
99
if (entity is IHasDateTimeOffset dt)
1010
dt.CreatedAt = DateTimeOffset.Now;
1111
return entity;
1212
});
1313

14-
private readonly TransactionOptions transactionOptions;
15-
private readonly TransactionScopeOption transactionScopeOption;
14+
private readonly TransactionOptions _transactionOptions;
15+
private readonly TransactionScopeOption _transactionScopeOption;
16+
private readonly IDbContextFactory<TDbContext> _dbContextFactory;
1617

17-
public Repository(TDbContext dbContext, TransactionScopeOption transactionScopeOption, System.Transactions.IsolationLevel isolationLevel)
18-
: base(dbContext)
18+
public Repository(IDbContextFactory<TDbContext> dbContextFactory
19+
, TransactionScopeOption transactionScopeOption
20+
, System.Transactions.IsolationLevel isolationLevel)
21+
: base(dbContextFactory.CreateDbContext())
1922
{
20-
this.transactionScopeOption = transactionScopeOption;
21-
this.transactionOptions = new() { IsolationLevel = isolationLevel };
23+
this._transactionScopeOption = transactionScopeOption;
24+
this._dbContextFactory = dbContextFactory;
25+
this._transactionOptions = new() { IsolationLevel = isolationLevel };
2226
}
2327

2428
public IQueryable<T> AsQueryable<T>() where T : class
@@ -28,7 +32,7 @@ public IQueryable<T> AsQueryable<T>() where T : class
2832

2933
private DbSet<T> CachedContextSet<T>() where T : class
3034
{
31-
return (DbSet<T>)cachedDbSets.GetOrAdd(typeof(T).FullName, CurrentDbContext.Set<T>());
35+
return (DbSet<T>)_cachedDbSets.GetOrAdd(typeof(T).FullName, DbContext.Set<T>());
3236
}
3337

3438
public void Delete<T>(T entity) where T : class
@@ -110,7 +114,7 @@ public void Insert<T>(T entity) where T : class
110114
{
111115
ArgumentNullException.ThrowIfNull(entity, nameof(entity));
112116

113-
CachedContextSet<T>().Add((T)funcCreatedAt(entity));
117+
CachedContextSet<T>().Add((T)_funcCreatedAt(entity));
114118
}
115119

116120
public void Insert<T>(params T[] entities) where T : class
@@ -127,7 +131,7 @@ public ValueTask<EntityEntry<T>> InsertAsync<T>(T entity, CancellationToken canc
127131
{
128132
ArgumentNullException.ThrowIfNull(entity, nameof(entity));
129133

130-
return CachedContextSet<T>().AddAsync((T)funcCreatedAt(entity), cancellationToken);
134+
return CachedContextSet<T>().AddAsync((T)_funcCreatedAt(entity), cancellationToken);
131135
}
132136

133137
public Task InsertAsync<T>(T[] entities, CancellationToken cancellationToken = default) where T : class
@@ -169,20 +173,25 @@ private void _InternalInsert<T>(IEnumerable<T> entities) where T : class
169173
{
170174
ArgumentNullException.ThrowIfNull(entities, nameof(entities));
171175

172-
CachedContextSet<T>().AddRange(entities.Select(f => (T)funcCreatedAt(f)));
176+
CachedContextSet<T>().AddRange(entities.Select(f => (T)_funcCreatedAt(f)));
173177
}
174178

175179
private Task _InternalInsertAsync<T>(IEnumerable<T> entities, CancellationToken cancellationToken = default) where T : class
176180
{
177181
ArgumentNullException.ThrowIfNull(entities, nameof(entities));
178182

179-
return CachedContextSet<T>().AddRangeAsync(entities.Select(f => (T)funcCreatedAt(f)), cancellationToken);
183+
return CachedContextSet<T>().AddRangeAsync(entities.Select(f => (T)_funcCreatedAt(f)), cancellationToken);
180184
}
181185

182186
private TransactionScope CreateTransactionScope()
183187
{
184188
TransactionScopeAsyncFlowOption transactionScopeAsyncFlowOption =
185-
transactionScopeOption == TransactionScopeOption.Suppress ? TransactionScopeAsyncFlowOption.Suppress : TransactionScopeAsyncFlowOption.Enabled;
186-
return new TransactionScope(transactionScopeOption, transactionOptions, transactionScopeAsyncFlowOption);
189+
_transactionScopeOption == TransactionScopeOption.Suppress ? TransactionScopeAsyncFlowOption.Suppress : TransactionScopeAsyncFlowOption.Enabled;
190+
return new TransactionScope(_transactionScopeOption, _transactionOptions, transactionScopeAsyncFlowOption);
191+
}
192+
193+
public override DbContext CreateDbContext()
194+
{
195+
return _dbContextFactory.CreateDbContext();
187196
}
188197
}

src/SampleDotnet.RepositoryFactory/RepositoryBase.cs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,22 @@ namespace SampleDotnet.RepositoryFactory;
44

55
internal abstract class RepositoryBase : IRepository
66
{
7-
private static readonly Func<object, object> funcUpdatedAt = new((entity) =>
7+
private static readonly Func<object, object> _funcUpdatedAt = new((entity) =>
88
{
99
if (entity is IHasDateTimeOffset dt)
1010
dt.UpdatedAt = DateTimeOffset.Now;
1111
return entity;
1212
});
1313

14-
private readonly DbContext _context;
15-
internal readonly ConcurrentDictionary<string, IQueryable> cachedDbSets = new();
14+
private DbContext context;
15+
internal readonly ConcurrentDictionary<string, IQueryable> _cachedDbSets = new();
1616

1717
protected RepositoryBase(DbContext context)
1818
{
19-
this._context = context;
19+
this.context = context;
2020
}
2121

22-
public DbContext CurrentDbContext => _context;
22+
public DbContext DbContext => context;
2323

2424
public virtual void Delete(object entity)
2525
{
@@ -30,12 +30,12 @@ public virtual void Delete(object entity)
3030

3131
public virtual void DeleteRange(params object[] entities)
3232
{
33-
_context.RemoveRange(entities);
33+
context.RemoveRange(entities);
3434
}
3535

3636
public void Dispose()
3737
{
38-
cachedDbSets.Clear();
38+
_cachedDbSets.Clear();
3939
GC.SuppressFinalize(this);
4040
}
4141

@@ -48,6 +48,17 @@ public virtual void Update(object entity)
4848

4949
public virtual void UpdateRange(params object[] entities)
5050
{
51-
_context.UpdateRange(entities.Select(f => funcUpdatedAt(f)));
51+
context.UpdateRange(entities.Select(f => _funcUpdatedAt(f)));
52+
}
53+
54+
public abstract DbContext CreateDbContext();
55+
56+
public DbContext RefreshDbContext()
57+
{
58+
try { context.Dispose(); } catch { }
59+
60+
context = CreateDbContext();
61+
62+
return context;
5263
}
5364
}

src/SampleDotnet.RepositoryFactory/UnitOfWork.cs

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
using System.Collections.Concurrent;
2+
using System.Collections.Generic;
23

34
namespace SampleDotnet.RepositoryFactory;
45

56
internal class UnitOfWork : IUnitOfWork
67
{
7-
private readonly Queue<DbContext> dbContextPool = new();
8-
private readonly ConcurrentDictionary<DbContextId, IRepository> repositoryPool = new();
9-
private readonly SemaphoreSlim semaphoreSlim = new(1, 1);
10-
private readonly IServiceProvider serviceProvider;
8+
private readonly Queue<DbContextId> _dbContextPool = new();
9+
private readonly ConcurrentDictionary<DbContextId, IRepository> _repositoryPool = new();
10+
private readonly SemaphoreSlim _semaphoreSlim = new(1, 1);
11+
private readonly IServiceProvider _serviceProvider;
1112
private bool disposedValue;
1213

1314
public UnitOfWork(IServiceProvider serviceProvider)
1415
{
15-
this.serviceProvider = serviceProvider;
16+
this._serviceProvider = serviceProvider;
1617
}
1718

1819
public bool IsDbConcurrencyExceptionThrown { get => SaveChangesException != null && SaveChangesException.Exception is DbUpdateConcurrencyException; }
@@ -22,14 +23,12 @@ public UnitOfWork(IServiceProvider serviceProvider)
2223
public IRepository<TDbContext> CreateRepository<TDbContext>(System.Transactions.TransactionScopeOption transactionScopeOption = System.Transactions.TransactionScopeOption.Required, System.Transactions.IsolationLevel isolationLevel = System.Transactions.IsolationLevel.ReadCommitted)
2324
where TDbContext : DbContext
2425
{
25-
var dbContext = serviceProvider
26-
.GetRequiredService<IDbContextFactory<TDbContext>>()
27-
.CreateDbContext();
28-
29-
dbContextPool.Enqueue(dbContext);
26+
var dbContext = _serviceProvider
27+
.GetRequiredService<IDbContextFactory<TDbContext>>();
3028

3129
var repository = new Repository<TDbContext>(dbContext, transactionScopeOption, isolationLevel);
32-
repositoryPool.TryAdd(dbContext.ContextId, repository);
30+
_dbContextPool.Enqueue(repository.DbContext.ContextId);
31+
_repositoryPool.TryAdd(repository.DbContext.ContextId, repository);
3332
return repository;
3433
}
3534

@@ -49,33 +48,45 @@ public async Task<bool> SaveChangesAsync(CancellationToken cancellationToken = d
4948
DbContext? thrownExceptionDbContext = null;
5049
try
5150
{
52-
await semaphoreSlim.WaitAsync(cancellationToken);
51+
await _semaphoreSlim.WaitAsync(cancellationToken);
5352

54-
int count = dbContextPool.Count;
55-
foreach (var dbContext in dbContextPool)
53+
int count = _dbContextPool.Count;
54+
foreach (var dbContextKey in _dbContextPool)
5655
{
57-
try
58-
{
59-
if (!dbContext.ChangeTracker.AutoDetectChangesEnabled)
60-
dbContext.ChangeTracker.DetectChanges();
61-
await dbContext.SaveChangesAsync(false, cancellationToken);
62-
}
63-
catch
56+
if (_repositoryPool.TryGetValue(dbContextKey, out var repo))
6457
{
65-
thrownExceptionDbContext = dbContext;
66-
foreach (var context in dbContextPool)
58+
try
59+
{
60+
if (!repo.ChangeTracker.AutoDetectChangesEnabled)
61+
repo.ChangeTracker.DetectChanges();
62+
await repo.DbContext.SaveChangesAsync(false, cancellationToken);
63+
}
64+
catch
6765
{
68-
await context.RollbackChangesAsync(false, cancellationToken);
66+
thrownExceptionDbContext = repo.DbContext;
67+
68+
foreach (var dbContextKey2 in _dbContextPool)
69+
{
70+
if (_repositoryPool.TryGetValue(dbContextKey2, out var repo2))
71+
{
72+
await repo2.DbContext.RollbackChangesAsync(false, cancellationToken);
73+
}
74+
}
75+
throw;
6976
}
70-
throw;
7177
}
7278
}
7379

7480
for (int i = 0; i < count; i++)
7581
{
76-
if (dbContextPool.TryDequeue(out var dbContext) && dbContext != null)
82+
if (_dbContextPool.TryDequeue(out var dbContextKey) && dbContextKey != null)
7783
{
78-
dbContext.SilentDbContextDispose(false/*try true if required*/);
84+
if (_repositoryPool.TryRemove(dbContextKey, out var repo))
85+
{
86+
var newDbContext = repo.RefreshDbContext();
87+
_dbContextPool.Enqueue(newDbContext.ContextId);
88+
_repositoryPool.TryAdd(newDbContext.ContextId, repo);
89+
}
7990
}
8091
}
8192
}
@@ -86,7 +97,7 @@ public async Task<bool> SaveChangesAsync(CancellationToken cancellationToken = d
8697
}
8798
finally
8899
{
89-
semaphoreSlim.Release();
100+
_semaphoreSlim.Release();
90101
}
91102
return true;
92103
}
@@ -97,17 +108,16 @@ protected virtual void Dispose(bool disposing)
97108
{
98109
if (disposing)
99110
{
100-
foreach (var key in repositoryPool.Keys)
111+
foreach (var key in _repositoryPool.Keys)
101112
{
102-
if (repositoryPool.TryRemove(key, out var repository))
113+
if (_repositoryPool.TryRemove(key, out var repository))
103114
{
104115
try { repository.Dispose(); } catch { }
105116
}
106117
}
107-
while (dbContextPool.TryDequeue(out var dbContext) && dbContext != null)
108-
dbContext.SilentDbContextDispose();
109118

110-
semaphoreSlim.Dispose();
119+
_dbContextPool.Clear();
120+
_semaphoreSlim.Dispose();
111121
}
112122
disposedValue = true;
113123
}

test/SampleDotnet.RepositoryFactory.Tests/Cases/DbContextDisposeTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public async Task Case_DbContext_Should_Not_Throw_ObjectDisposedException()
2626
//cnnBuilder.ConnectTimeout = TimeSpan.FromMinutes(5).Seconds;
2727
//options.UseSqlServer(cnnBuilder.ToString());
2828

29-
options.UseInMemoryDatabase("Case_UnitOfWork_Rollback");
29+
options.UseInMemoryDatabase("Case_DbContext_Should_Not_Throw_ObjectDisposedException");
3030
options.EnableSensitiveDataLogging();
3131
options.EnableDetailedErrors();
3232
});

0 commit comments

Comments
 (0)