From dcd643ea789808c94822a65b4c67f94e70e305be Mon Sep 17 00:00:00 2001 From: "m.rahimian" Date: Mon, 3 Aug 2026 15:14:26 +0330 Subject: [PATCH 01/11] add async repository methods --- .../RepositoryBase.cs | 251 ++++++++++++++--- .../RepositoryBase.cs | 262 +++++++++++++----- Source/BSN.Commons.Orm.Redis/DbContext.cs | 17 +- .../BSN.Commons.Orm.Redis/RepositoryBase.cs | 235 +++++++++++++--- .../BSN.Commons/Infrastructure/IDbContext.cs | 15 +- .../BSN.Commons/Infrastructure/IRepository.cs | 62 ++++- .../BSN.Commons/Infrastructure/IUnitOfWork.cs | 15 +- .../BSN.Commons/Infrastructure/UnitOfWork.cs | 180 +++++++++++- .../Data/UnitTestContext.cs | 13 +- .../Mock/UsersRepsitory.cs | 3 +- .../UnitOfWorkTest.cs | 11 +- .../Infrastructure/DatabaseFactory.cs | 12 +- .../Mock/UserRepository.cs | 1 - 13 files changed, 877 insertions(+), 200 deletions(-) diff --git a/Source/BSN.Commons.Orm.EntityFramework/RepositoryBase.cs b/Source/BSN.Commons.Orm.EntityFramework/RepositoryBase.cs index acad5be..c0e3b1a 100644 --- a/Source/BSN.Commons.Orm.EntityFramework/RepositoryBase.cs +++ b/Source/BSN.Commons.Orm.EntityFramework/RepositoryBase.cs @@ -1,46 +1,84 @@ -using System; +using BSN.Commons.Infrastructure; +using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; namespace BSN.Commons.Orm.EntityFramework { - using BSN.Commons.Infrastructure; - /// - public abstract partial class RepositoryBase : IRepository where T : class + public abstract partial class RepositoryBase : IRepository + where T : class { /// protected RepositoryBase(IDatabaseFactory databaseFactory) { + if (databaseFactory == null) + throw new ArgumentNullException(nameof(databaseFactory)); + DatabaseFactory = databaseFactory; dbSet = DataContext.Set(); } + /// public virtual void Add(T entity) { dbSet.Add(entity); } + + /// + public virtual Task AddAsync( + T entity, + CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + + dbSet.Add(entity); + + return Task.CompletedTask; + } + + /// public virtual void AddRange(IEnumerable entities) { dbSet.AddRange(entities); } + + /// + public virtual Task AddRangeAsync( + IEnumerable entities, + CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + + dbSet.AddRange(entities); + + return Task.CompletedTask; + } + + /// public virtual void Update(T entity) { Update(entity, cfg => cfg.IncludeAllProperties()); } + /// - public virtual void Update(T entity, Action> configurer) + public virtual void Update( + T entity, + Action> configurer) { var updateConfig = new UpdateConfig(); - configurer.Invoke(updateConfig); + + configurer(updateConfig); if (updateConfig.AutoDetectChangedPropertiesEnabled) { @@ -48,7 +86,8 @@ public virtual void Update(T entity, Action> configurer) return; } - bool autoDetectChangesPreviousValue = _dataContext.Configuration.AutoDetectChangesEnabled; + bool previousValue = + _dataContext.Configuration.AutoDetectChangesEnabled; try { @@ -58,127 +97,249 @@ public virtual void Update(T entity, Action> configurer) if (updateConfig.IncludeAllPropertiesEnabled) { - _dataContext.Entry(entity).State = EntityState.Modified; + _dataContext.Entry(entity).State = + EntityState.Modified; } else { foreach (string propertyName in updateConfig.PropertyNames) - _dataContext.Entry(entity).Property(propertyName).IsModified = true; + { + _dataContext + .Entry(entity) + .Property(propertyName) + .IsModified = true; + } } } finally { - _dataContext.Configuration.AutoDetectChangesEnabled = autoDetectChangesPreviousValue; + _dataContext.Configuration.AutoDetectChangesEnabled = + previousValue; } } + /// public virtual void UpdateRange(IEnumerable entities) { - UpdateRange(entities, cfg => cfg.IncludeAllProperties()); + UpdateRange( + entities, + cfg => cfg.IncludeAllProperties()); } + /// - public virtual void UpdateRange(IEnumerable entities, Action> configurer) + public virtual void UpdateRange( + IEnumerable entities, + Action> configurer) { var updateConfig = new UpdateConfig(); - configurer.Invoke(updateConfig); - if (updateConfig.AutoDetectChangedPropertiesEnabled) - { - _dataContext.Configuration.AutoDetectChangesEnabled = true; - return; - } + configurer(updateConfig); - bool autoDetectChangesPreviousValue = _dataContext.Configuration.AutoDetectChangesEnabled; + bool previousValue = + _dataContext.Configuration.AutoDetectChangesEnabled; try { _dataContext.Configuration.AutoDetectChangesEnabled = false; - if (updateConfig.IncludeAllPropertiesEnabled) + foreach (T entity in entities) { - foreach (T entity in entities) + dbSet.Attach(entity); + + if (updateConfig.IncludeAllPropertiesEnabled) { - dbSet.Attach(entity); - _dataContext.Entry(entity).State = EntityState.Modified; + _dataContext.Entry(entity).State = + EntityState.Modified; } - } - else - { - foreach (T entity in entities) + else { - dbSet.Attach(entity); foreach (string propertyName in updateConfig.PropertyNames) - _dataContext.Entry(entity).Property(propertyName).IsModified = true; + { + _dataContext + .Entry(entity) + .Property(propertyName) + .IsModified = true; + } } } } finally { - _dataContext.Configuration.AutoDetectChangesEnabled = autoDetectChangesPreviousValue; + _dataContext.Configuration.AutoDetectChangesEnabled = + previousValue; } } + /// public virtual void Delete(T entity) { dbSet.Remove(entity); } + /// - public virtual void Delete(Expression> where) + public virtual void Delete( + Expression> where) { - var objects = dbSet.Where(where).AsEnumerable(); - foreach (var obj in objects) + var objects = dbSet.Where(where); + + foreach (T obj in objects) + { dbSet.Remove(obj); + } } + /// - public virtual void DeleteRange(IEnumerable entities) + public virtual void DeleteRange( + IEnumerable entities) { dbSet.RemoveRange(entities); } + /// - public virtual T GetById(KeyType id) + public virtual T GetById( + KeyType id) { return dbSet.Find(id); } + /// - public virtual IEnumerable GetAll() + public virtual Task GetByIdAsync( + KeyType id, + CancellationToken cancellationToken = default(CancellationToken)) { - return dbSet.ToList(); + cancellationToken.ThrowIfCancellationRequested(); + + return dbSet.FindAsync( + cancellationToken, + id); } + /// - public virtual IEnumerable GetMany(Expression> where) + public virtual IEnumerable GetAll( + bool asNoTracking = false) { - return dbSet.Where(where); + IQueryable query = dbSet; + + if (asNoTracking) + query = query.AsNoTracking(); + + return query.ToList(); } + /// - public T Get(Expression> where) + public virtual async Task> GetAllAsync( + bool asNoTracking = false, + CancellationToken cancellationToken = default(CancellationToken)) { - return dbSet.Where(where).FirstOrDefault(); + IQueryable query = dbSet; + + if (asNoTracking) + query = query.AsNoTracking(); + + return await query + .ToListAsync(cancellationToken); } + + /// + public virtual IEnumerable GetMany( + Expression> where, + bool asNoTracking = false) + { + IQueryable query = dbSet.Where(where); + + if (asNoTracking) + query = query.AsNoTracking(); + + return query; + } + + + /// + public virtual async Task> GetManyAsync( + Expression> where, + bool asNoTracking = false, + CancellationToken cancellationToken = default(CancellationToken)) + { + IQueryable query = dbSet.Where(where); + + if (asNoTracking) + query = query.AsNoTracking(); + + return await query + .ToListAsync(cancellationToken); + } + + + /// + public virtual T Get( + Expression> where, + bool asNoTracking = false) + { + IQueryable query = dbSet.Where(where); + + if (asNoTracking) + query = query.AsNoTracking(); + + return query.FirstOrDefault(); + } + + + /// + public virtual async Task GetAsync( + Expression> where, + bool asNoTracking = false, + CancellationToken cancellationToken = default(CancellationToken)) + { + IQueryable query = dbSet.Where(where); + + if (asNoTracking) + query = query.AsNoTracking(); + + return await query + .FirstOrDefaultAsync(cancellationToken); + } + + /// - /// TODO: complete doc + /// Database Set /// protected readonly DbSet dbSet; + /// - /// TODO: complete doc + /// Database Context /// - protected DbContext DataContext => _dataContext ?? (_dataContext = (DbContext)DatabaseFactory.Get()); + protected DbContext DataContext + { + get + { + if (_dataContext == null) + { + _dataContext = + (DbContext)DatabaseFactory.Get(); + } + + return _dataContext; + } + } + /// - /// TODO: complete doc + /// Database Factory /// protected IDatabaseFactory DatabaseFactory { get; private set; } + private DbContext _dataContext; } } \ No newline at end of file diff --git a/Source/BSN.Commons.Orm.EntityFrameworkCore/RepositoryBase.cs b/Source/BSN.Commons.Orm.EntityFrameworkCore/RepositoryBase.cs index f3b2af8..67472fd 100644 --- a/Source/BSN.Commons.Orm.EntityFrameworkCore/RepositoryBase.cs +++ b/Source/BSN.Commons.Orm.EntityFrameworkCore/RepositoryBase.cs @@ -4,93 +4,221 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; namespace BSN.Commons.Orm.EntityFrameworkCore { /// - public class RepositoryBase : IRepository where T : class + public class RepositoryBase : IRepository + where T : class { - /// + protected readonly DbSet dbSet; + + protected DbContext _dataContext; + + protected RepositoryBase(IDatabaseFactory databaseFactory) { + if (databaseFactory == null) + throw new ArgumentNullException(nameof(databaseFactory)); + DatabaseFactory = databaseFactory; dbSet = DataContext.Set(); } - /// + + protected DbContext DataContext + { + get + { + if (_dataContext == null) + _dataContext = (DbContext)DatabaseFactory.Get(); + + return _dataContext; + } + } + + + protected IDatabaseFactory DatabaseFactory { get; private set; } + + public void Add(T entity) { dbSet.Add(entity); } - /// + + public async Task AddAsync( + T entity, + CancellationToken cancellationToken = default(CancellationToken)) + { + await dbSet + .AddAsync(entity, cancellationToken) + .ConfigureAwait(false); + } + + public void AddRange(IEnumerable entities) { dbSet.AddRange(entities); } - /// + + public Task AddRangeAsync( + IEnumerable entities, + CancellationToken cancellationToken = default(CancellationToken)) + { + return dbSet.AddRangeAsync( + entities, + cancellationToken); + } + + public void Delete(T entity) { dbSet.Remove(entity); } - /// + public void Delete(Expression> where) { - dbSet.RemoveRange(dbSet.Where(where)); + dbSet.RemoveRange( + dbSet.Where(where)); } - /// + public void DeleteRange(IEnumerable entities) { dbSet.RemoveRange(entities); } - /// - public virtual T GetById(KeyType id) + + public virtual T GetById( + KeyType id) { return dbSet.Find(id); } - /// - public virtual IEnumerable GetAll() + + public virtual async Task GetByIdAsync( + KeyType id, + CancellationToken cancellationToken = default(CancellationToken)) { - return dbSet.ToList(); + return await dbSet + .FindAsync(id , + cancellationToken) + .ConfigureAwait(false); } - /// - public virtual IEnumerable GetMany(Expression> where) + + public virtual IEnumerable GetAll( + bool asNoTracking = false) { - return dbSet.Where(where); + IQueryable query = dbSet; + + if (asNoTracking) + query = query.AsNoTracking(); + + return query.ToList(); + } + + + public virtual async Task> GetAllAsync( + bool asNoTracking = false, + CancellationToken cancellationToken = default(CancellationToken)) + { + IQueryable query = dbSet; + + if (asNoTracking) + query = query.AsNoTracking(); + + return await query + .ToListAsync(cancellationToken) + .ConfigureAwait(false); } - /// - public T Get(Expression> where) + + public virtual IEnumerable GetMany( + Expression> where, + bool asNoTracking = false) { - return dbSet.Where(where).FirstOrDefault(); + IQueryable query = dbSet.Where(where); + + if (asNoTracking) + query = query.AsNoTracking(); + + return query; } - /// + + public virtual async Task> GetManyAsync( + Expression> where, + bool asNoTracking = false, + CancellationToken cancellationToken = default(CancellationToken)) + { + IQueryable query = dbSet.Where(where); + + if (asNoTracking) + query = query.AsNoTracking(); + + return await query + .ToListAsync(cancellationToken) + .ConfigureAwait(false); + } + + + public T Get( + Expression> where, + bool asNoTracking = false) + { + IQueryable query = dbSet.Where(where); + + if (asNoTracking) + query = query.AsNoTracking(); + + return query.FirstOrDefault(); + } + + + public async Task GetAsync( + Expression> where, + bool asNoTracking = false, + CancellationToken cancellationToken = default(CancellationToken)) + { + IQueryable query = dbSet.Where(where); + + if (asNoTracking) + query = query.AsNoTracking(); + + return await query + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + } + + public void Update(T entity) { Update(entity, cfg => cfg.IncludeAllProperties()); } - /// - public void Update(T entity, Action> configurer) + + public void Update( + T entity, + Action> configurer) { var updateConfig = new UpdateConfig(); - configurer.Invoke(updateConfig); - // TODO: Why this behaviour exist? + configurer(updateConfig); + if (updateConfig.AutoDetectChangedPropertiesEnabled) { _dataContext.ChangeTracker.AutoDetectChangesEnabled = true; return; } - bool autoDetectChangesPreviousValue = _dataContext.ChangeTracker.AutoDetectChangesEnabled; + bool previous = + _dataContext.ChangeTracker.AutoDetectChangesEnabled; try { @@ -100,83 +228,77 @@ public void Update(T entity, Action> configurer) if (updateConfig.IncludeAllPropertiesEnabled) { - _dataContext.Entry(entity).State = EntityState.Modified; + _dataContext.Entry(entity).State = + EntityState.Modified; } else { - foreach (string propertyName in updateConfig.PropertyNames) - _dataContext.Entry(entity).Property(propertyName).IsModified = true; + foreach (var propertyName in updateConfig.PropertyNames) + { + _dataContext + .Entry(entity) + .Property(propertyName) + .IsModified = true; + } } } finally { - _dataContext.ChangeTracker.AutoDetectChangesEnabled = autoDetectChangesPreviousValue; + _dataContext.ChangeTracker.AutoDetectChangesEnabled = + previous; } } - /// + public void UpdateRange(IEnumerable entities) { - UpdateRange(entities, cfg => cfg.IncludeAllProperties()); + UpdateRange( + entities, + cfg => cfg.IncludeAllProperties()); } - /// - public void UpdateRange(IEnumerable entities, Action> configurer) + + public void UpdateRange( + IEnumerable entities, + Action> configurer) { var updateConfig = new UpdateConfig(); - configurer.Invoke(updateConfig); - if (updateConfig.AutoDetectChangedPropertiesEnabled) - { - _dataContext.ChangeTracker.AutoDetectChangesEnabled = true; - return; - } + configurer(updateConfig); - bool autoDetectChangesPreviousValue = _dataContext.ChangeTracker.AutoDetectChangesEnabled; + bool previous = + _dataContext.ChangeTracker.AutoDetectChangesEnabled; try { _dataContext.ChangeTracker.AutoDetectChangesEnabled = false; - if (updateConfig.IncludeAllPropertiesEnabled) + foreach (var entity in entities) { - foreach (T entity in entities) + dbSet.Attach(entity); + + if (updateConfig.IncludeAllPropertiesEnabled) { - dbSet.Attach(entity); - _dataContext.Entry(entity).State = EntityState.Modified; + _dataContext.Entry(entity).State = + EntityState.Modified; } - } - else - { - foreach (T entity in entities) + else { - dbSet.Attach(entity); - foreach (string propertyName in updateConfig.PropertyNames) - _dataContext.Entry(entity).Property(propertyName).IsModified = true; + foreach (var propertyName in updateConfig.PropertyNames) + { + _dataContext + .Entry(entity) + .Property(propertyName) + .IsModified = true; + } } } } finally { - _dataContext.ChangeTracker.AutoDetectChangesEnabled = autoDetectChangesPreviousValue; + _dataContext.ChangeTracker.AutoDetectChangesEnabled = + previous; } } - - /// - /// TODO: complete doc - /// - protected readonly DbSet dbSet; - - /// - /// TODO: complete doc - /// - protected DbContext DataContext => _dataContext ?? (_dataContext = (DbContext)DatabaseFactory.Get()); - - /// - /// TODO: complete doc - /// - protected IDatabaseFactory DatabaseFactory { get; private set; } - - private DbContext _dataContext; } -} +} \ No newline at end of file diff --git a/Source/BSN.Commons.Orm.Redis/DbContext.cs b/Source/BSN.Commons.Orm.Redis/DbContext.cs index a16a325..e215ed0 100644 --- a/Source/BSN.Commons.Orm.Redis/DbContext.cs +++ b/Source/BSN.Commons.Orm.Redis/DbContext.cs @@ -2,9 +2,8 @@ using BSN.Commons.Infrastructure.Redis; using Microsoft.Extensions.Options; using Redis.OM; -using Redis.OM.Contracts; -using Redis.OM.Searching; -using StackExchange.Redis; +using System.Threading; +using System.Threading.Tasks; namespace BSN.Commons.Orm.Redis { @@ -38,10 +37,22 @@ public virtual int SaveChanges() throw new System.NotImplementedException("We don't have a way to save changes on redis om yet."); } + /// + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException("We don't have a way to save changes on redis om yet."); + } + /// public void Dispose() { // TODO release managed resources here } + + public ValueTask DisposeAsync() + { + // TODO release managed resources here + return new ValueTask(); + } } } \ No newline at end of file diff --git a/Source/BSN.Commons.Orm.Redis/RepositoryBase.cs b/Source/BSN.Commons.Orm.Redis/RepositoryBase.cs index 5ea0526..800033b 100644 --- a/Source/BSN.Commons.Orm.Redis/RepositoryBase.cs +++ b/Source/BSN.Commons.Orm.Redis/RepositoryBase.cs @@ -1,15 +1,13 @@ -using System; -using System.Linq; -using System.Linq.Expressions; -using System.Collections.Generic; - +using BSN.Commons.Infrastructure; using Redis.OM; using Redis.OM.Contracts; using Redis.OM.Searching; - -using BSN.Commons.Infrastructure; -using BSN.Commons.Infrastructure.Redis; -using System.Data.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; namespace BSN.Commons.Orm.Redis { @@ -17,7 +15,8 @@ namespace BSN.Commons.Orm.Redis /// Repository Base for Redis Implementation /// /// - public class RepositoryBase : IRepository where T : class + public class RepositoryBase : IRepository + where T : class { /// /// Constructor for Redis Repository Base @@ -25,113 +24,279 @@ public class RepositoryBase : IRepository where T : class /// Database Factory Containing an IRedisDbContext protected RepositoryBase(IDatabaseFactory databaseFactory) { + if (databaseFactory == null) + throw new ArgumentNullException(nameof(databaseFactory)); + DatabaseFactory = databaseFactory; + dbCollection = DataContext.RedisCollection(); - // TODO: Check that IndexCreationService is necessary or not. DataContext.Connection.CreateIndex(typeof(T)); } /// - public void Add(T entity) + public virtual void Add(T entity) { + if (entity == null) + throw new ArgumentNullException(nameof(entity)); + dbCollection.Insert(entity); } /// - public void AddRange(IEnumerable entities) + public virtual Task AddAsync( + T entity, + CancellationToken cancellationToken = default(CancellationToken)) { + cancellationToken.ThrowIfCancellationRequested(); + + Add(entity); + + return Task.CompletedTask; + } + + + /// + public virtual void AddRange( + IEnumerable entities) + { + if (entities == null) + throw new ArgumentNullException(nameof(entities)); + foreach (var entity in entities) { Add(entity); } } + /// - public void Update(T entity) + public virtual Task AddRangeAsync( + IEnumerable entities, + CancellationToken cancellationToken = default(CancellationToken)) { + cancellationToken.ThrowIfCancellationRequested(); + + AddRange(entities); + + return Task.CompletedTask; + } + + + /// + public virtual void Update(T entity) + { + if (entity == null) + throw new ArgumentNullException(nameof(entity)); + dbCollection.Update(entity); } + /// - public void Update(T entity, Action> configurer) + public virtual void Update( + T entity, + Action> configurer) { - throw new NotImplementedException("We don't have a way to update with a configuration on redis"); + throw new NotSupportedException( + "Redis repository does not support partial update configuration."); } + /// - public void UpdateRange(IEnumerable entities) + public virtual void UpdateRange( + IEnumerable entities) { + if (entities == null) + throw new ArgumentNullException(nameof(entities)); + foreach (var entity in entities) { Update(entity); } } + /// - public void UpdateRange(IEnumerable entities, Action> configurer) + public virtual void UpdateRange( + IEnumerable entities, + Action> configurer) { - throw new NotImplementedException("We don't have a way to update range with a configuration on redis"); + throw new NotSupportedException( + "Redis repository does not support partial update configuration."); } + /// - public void Delete(T entity) + public virtual void Delete(T entity) { + if (entity == null) + throw new ArgumentNullException(nameof(entity)); + dbCollection.Delete(entity); } + /// - public void Delete(Expression> where) + public virtual void Delete( + Expression> where) { - DeleteRange(dbCollection.Where(where)); + if (where == null) + throw new ArgumentNullException(nameof(where)); + + DeleteRange( + dbCollection.Where(where)); } + /// - public void DeleteRange(IEnumerable entities) + public virtual void DeleteRange( + IEnumerable entities) { + if (entities == null) + throw new ArgumentNullException(nameof(entities)); + dbCollection.Delete(entities); } + /// - public T GetById(KeyType id) + public virtual T GetById( + KeyType id) { - if (id is string str_id) + if (id == null) + throw new ArgumentNullException(nameof(id)); + + + if (id is string) { - T? entity = dbCollection.FindById(str_id); + var entity = + dbCollection.FindById(id.ToString()); + if (entity == null) { - throw new KeyNotFoundException($"entity with key of {id} was not found."); + throw new KeyNotFoundException( + $"Entity with key {id} was not found."); } return entity; } - throw new NotImplementedException($"KeyType of {typeof(KeyType)} is not supported."); + + throw new NotSupportedException( + $"Redis repository does not support key type {typeof(KeyType)}."); } + /// - public T Get(Expression> where) + public virtual Task GetByIdAsync( + KeyType id, + CancellationToken cancellationToken = default(CancellationToken)) { - return dbCollection.Where(where).FirstOrDefault(); + cancellationToken.ThrowIfCancellationRequested(); + + return Task.FromResult( + GetById(id)); } + + public virtual T Get( + Expression> where, + bool asNoTracking = false) + { + if (where == null) + throw new ArgumentNullException(nameof(where)); + + return dbCollection + .Where(where) + .FirstOrDefault(); + } + + /// - public IEnumerable GetAll() + public virtual Task GetAsync( + Expression> where, + bool asNoTracking = false, + CancellationToken cancellationToken = default(CancellationToken)) { - return dbCollection.Where(entity => true); + cancellationToken.ThrowIfCancellationRequested(); + + return Task.FromResult( + Get(where, asNoTracking)); } + + /// + public virtual IEnumerable GetAll( + bool asNoTracking = false) + { + return dbCollection + .Where(x => true); + } + + + /// + public virtual Task> GetAllAsync( + bool asNoTracking = false, + CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + + return Task.FromResult( + GetAll(asNoTracking)); + } + + /// - public IEnumerable GetMany(Expression> where) + public virtual IEnumerable GetMany( + Expression> where, + bool asNoTracking = false) { + if (where == null) + throw new ArgumentNullException(nameof(where)); + return dbCollection.Where(where); } + + /// + public virtual Task> GetManyAsync( + Expression> where, + bool asNoTracking = false, + CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + + return Task.FromResult( + GetMany(where, asNoTracking)); + } + + + protected readonly IRedisCollection dbCollection; - protected IDatabaseFactory DatabaseFactory { get; private set; } - protected IRedisConnectionProvider DataContext => _dataContext ?? (_dataContext = (IRedisConnectionProvider)DatabaseFactory.Get()); + protected IDatabaseFactory DatabaseFactory + { + get; + private set; + } + + + protected IRedisConnectionProvider DataContext + { + get + { + if (_dataContext == null) + { + _dataContext = + (IRedisConnectionProvider)DatabaseFactory.Get(); + } + + return _dataContext; + } + } + private IRedisConnectionProvider _dataContext; } diff --git a/Source/BSN.Commons/Infrastructure/IDbContext.cs b/Source/BSN.Commons/Infrastructure/IDbContext.cs index beeca36..dcc8c76 100644 --- a/Source/BSN.Commons/Infrastructure/IDbContext.cs +++ b/Source/BSN.Commons/Infrastructure/IDbContext.cs @@ -1,16 +1,23 @@ using System; +using System.Threading; +using System.Threading.Tasks; namespace BSN.Commons.Infrastructure { /// /// Interface for Database Context /// - public interface IDbContext : IDisposable + public interface IDbContext : IDisposable, IAsyncDisposable { /// - /// Save changes to the database + /// Save changes to the database. /// - /// int SaveChanges(); + + /// + /// Save changes to the database asynchronously. + /// + Task SaveChangesAsync( + CancellationToken cancellationToken = default); } -} +} \ No newline at end of file diff --git a/Source/BSN.Commons/Infrastructure/IRepository.cs b/Source/BSN.Commons/Infrastructure/IRepository.cs index ac9ea2d..29ae691 100644 --- a/Source/BSN.Commons/Infrastructure/IRepository.cs +++ b/Source/BSN.Commons/Infrastructure/IRepository.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; namespace BSN.Commons.Infrastructure { @@ -76,20 +78,72 @@ public interface IRepository where T : class /// Get Object by Expression. /// /// Expression + /// No Tracking /// Retrived Object or null - T Get(Expression> where); + T Get(Expression> where, bool asNoTracking = false); /// /// Get all Objects in the current repository. /// + /// No Tracking /// List of all Objects - IEnumerable GetAll(); + IEnumerable GetAll(bool asNoTracking = false); /// /// Get List of existing objects using Expression. /// /// Expression + /// No Tracking /// List of Objects - IEnumerable GetMany(Expression> where); - } + IEnumerable GetMany(Expression> where, bool asNoTracking = false); + + /// + /// Add new object asynchronously. + /// + Task AddAsync( + T entity, + CancellationToken cancellationToken = default(CancellationToken)); + + + /// + /// Add a range of objects asynchronously. + /// + Task AddRangeAsync( + IEnumerable entities, + CancellationToken cancellationToken = default(CancellationToken)); + + + /// + /// Get object by identifier asynchronously. + /// + Task GetByIdAsync( + KeyType id, + CancellationToken cancellationToken = default(CancellationToken)); + + + /// + /// Get object using expression asynchronously. + /// + Task GetAsync( + Expression> where, + bool asNoTracking = false, + CancellationToken cancellationToken = default(CancellationToken)); + + + /// + /// Get all objects asynchronously. + /// + Task> GetAllAsync( + bool asNoTracking = false, + CancellationToken cancellationToken = default(CancellationToken)); + + + /// + /// Get objects using expression asynchronously. + /// + Task> GetManyAsync( + Expression> where, + bool asNoTracking = false, + CancellationToken cancellationToken = default(CancellationToken)); + } } diff --git a/Source/BSN.Commons/Infrastructure/IUnitOfWork.cs b/Source/BSN.Commons/Infrastructure/IUnitOfWork.cs index e963c81..170bb51 100644 --- a/Source/BSN.Commons/Infrastructure/IUnitOfWork.cs +++ b/Source/BSN.Commons/Infrastructure/IUnitOfWork.cs @@ -1,14 +1,21 @@ using System; +using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; -using System.Transactions; namespace BSN.Commons.Infrastructure { - public interface IUnitOfWork + public interface IUnitOfWork : IDisposable, IAsyncDisposable { IDatabaseFactory DatabaseFactory { get; } - void Commit(); + IReadOnlyCollection Exceptions { get; } + void AddToQueue(ITaskUnit task); + + void Commit(); + + Task CommitAsync( + CancellationToken cancellationToken = default); } -} +} \ No newline at end of file diff --git a/Source/BSN.Commons/Infrastructure/UnitOfWork.cs b/Source/BSN.Commons/Infrastructure/UnitOfWork.cs index bc2a46b..fc96669 100644 --- a/Source/BSN.Commons/Infrastructure/UnitOfWork.cs +++ b/Source/BSN.Commons/Infrastructure/UnitOfWork.cs @@ -1,35 +1,70 @@ using System; using System.Collections.Generic; -using System.Transactions; using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Transactions; namespace BSN.Commons.Infrastructure { - public class UnitOfWork : IUnitOfWork + public class UnitOfWork : IUnitOfWork, IDisposable, IAsyncDisposable { - public IDatabaseFactory DatabaseFactory { get; } + private readonly Queue _tasks; + private readonly List _exceptions; - public List Exceptions { get; private set; } + private IDbContext _dataContext; + private bool _disposed; + + public IDatabaseFactory DatabaseFactory { get; private set; } - protected IDbContext DataContext => _dataContext ?? (_dataContext = DatabaseFactory.Get()); + public IReadOnlyCollection Exceptions + { + get { return _exceptions.AsReadOnly(); } + } + + protected IDbContext DataContext + { + get + { + if (_dataContext == null) + { + _dataContext = DatabaseFactory.Get(); + } + + return _dataContext; + } + } public UnitOfWork(IDatabaseFactory databaseFactory) { + if (databaseFactory == null) + throw new ArgumentNullException(nameof(databaseFactory)); + DatabaseFactory = databaseFactory; + _tasks = new Queue(); - Exceptions = new List(); + _exceptions = new List(); } + public void AddToQueue(ITaskUnit task) { - task = task ?? throw new ArgumentNullException(nameof(task)); + if (task == null) + throw new ArgumentNullException(nameof(task)); + + ThrowIfDisposed(); + _tasks.Enqueue(task); } + public void Commit() { - Queue executedTasks = new Queue(); + ThrowIfDisposed(); + + Queue executedTasks = + new Queue(); try { @@ -37,26 +72,141 @@ public void Commit() { while (_tasks.Count > 0) { - var task = _tasks.Dequeue(); + ITaskUnit task = _tasks.Dequeue(); + executedTasks.Enqueue(task); - Transaction.Current.EnlistVolatile(task, EnlistmentOptions.None); + + Transaction.Current.EnlistVolatile( + task, + EnlistmentOptions.None); } DataContext.SaveChanges(); + transaction.Complete(); } } - catch (Exception ex) + catch { - throw ex; + throw; } finally { - Exceptions.AddRange(executedTasks.Select(a => a.Exception)); + CollectExceptions(executedTasks); } } - private IDbContext _dataContext; - private readonly Queue _tasks; + + public async Task CommitAsync( + CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + + Queue executedTasks = + new Queue(); + + try + { + using (var transaction = new TransactionScope( + TransactionScopeOption.Required, + new TransactionOptions + { + IsolationLevel = IsolationLevel.ReadCommitted + }, + TransactionScopeAsyncFlowOption.Enabled)) + { + while (_tasks.Count > 0) + { + ITaskUnit task = _tasks.Dequeue(); + + executedTasks.Enqueue(task); + + Transaction.Current.EnlistVolatile( + task, + EnlistmentOptions.None); + } + + await DataContext.SaveChangesAsync(cancellationToken); + + transaction.Complete(); + } + } + catch + { + throw; + } + finally + { + CollectExceptions(executedTasks); + } + } + + + private void CollectExceptions( + IEnumerable executedTasks) + { + _exceptions.Clear(); + + foreach (ITaskUnit task in executedTasks) + { + if (task.Exception != null) + { + _exceptions.Add(task.Exception); + } + } + } + + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + if (_dataContext != null) + { + _dataContext.Dispose(); + _dataContext = null; + } + } + + _disposed = true; + } + + + public void Dispose() + { + Dispose(true); + + GC.SuppressFinalize(this); + } + + + public async ValueTask DisposeAsync() + { + if (_disposed) + return; + + if (_dataContext != null) + { + await _dataContext.DisposeAsync(); + _dataContext = null; + } + + _disposed = true; + + GC.SuppressFinalize(this); + } + + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException( + GetType().FullName); + } + } } } \ No newline at end of file diff --git a/Test/BSN.Commons.Orm.EntityFramework.Tests/Data/UnitTestContext.cs b/Test/BSN.Commons.Orm.EntityFramework.Tests/Data/UnitTestContext.cs index 6f0e243..d68093f 100644 --- a/Test/BSN.Commons.Orm.EntityFramework.Tests/Data/UnitTestContext.cs +++ b/Test/BSN.Commons.Orm.EntityFramework.Tests/Data/UnitTestContext.cs @@ -1,6 +1,7 @@ using BSN.Commons.Infrastructure; using BSN.Commons.Tests; using System.Data.Entity; +using System.Threading.Tasks; namespace BSN.Commons.Test.Data { @@ -36,5 +37,15 @@ public override int SaveChanges() { return base.SaveChanges(); } - } + public override Task SaveChangesAsync() + { + return base.SaveChangesAsync(); + } + + public ValueTask DisposeAsync() + { + base.Dispose(); + return new ValueTask(); + } + } } diff --git a/Test/BSN.Commons.Orm.EntityFramework.Tests/Mock/UsersRepsitory.cs b/Test/BSN.Commons.Orm.EntityFramework.Tests/Mock/UsersRepsitory.cs index 1f6ab48..d596fec 100644 --- a/Test/BSN.Commons.Orm.EntityFramework.Tests/Mock/UsersRepsitory.cs +++ b/Test/BSN.Commons.Orm.EntityFramework.Tests/Mock/UsersRepsitory.cs @@ -1,5 +1,4 @@ -using BSN.Commons.Test.Data; -using BSN.Commons.Infrastructure; +using BSN.Commons.Infrastructure; using BSN.Commons.Orm.EntityFramework; using BSN.Commons.Tests; diff --git a/Test/BSN.Commons.Orm.EntityFramework.Tests/UnitOfWorkTest.cs b/Test/BSN.Commons.Orm.EntityFramework.Tests/UnitOfWorkTest.cs index f22ab06..947b20a 100644 --- a/Test/BSN.Commons.Orm.EntityFramework.Tests/UnitOfWorkTest.cs +++ b/Test/BSN.Commons.Orm.EntityFramework.Tests/UnitOfWorkTest.cs @@ -1,13 +1,8 @@ -using BSN.Commons.Test.Infrastructure; -using BSN.Commons.Infrastructure; -using NUnit.Framework; -using BSN.Commons.Test.Data; -using System.Collections.Generic; +using BSN.Commons.Infrastructure; +using BSN.Commons.Test.Infrastructure; using BSN.Commons.Test.Mock; -using System; -using System.Threading.Tasks; -using System.Linq; using BSN.Commons.Tests; +using NUnit.Framework; namespace BSN.Commons.Test { diff --git a/Test/BSN.Commons.Orm.Redis.Tests/Infrastructure/DatabaseFactory.cs b/Test/BSN.Commons.Orm.Redis.Tests/Infrastructure/DatabaseFactory.cs index 30e3a78..fabfd6e 100644 --- a/Test/BSN.Commons.Orm.Redis.Tests/Infrastructure/DatabaseFactory.cs +++ b/Test/BSN.Commons.Orm.Redis.Tests/Infrastructure/DatabaseFactory.cs @@ -1,13 +1,9 @@ -using BSN.Commons.Test.Data; -using BSN.Commons.Infrastructure; -using System; -using System.Collections.Generic; -using System.Text; +using BSN.Commons.Infrastructure; using BSN.Commons.Infrastructure.Redis; -using Redis.OM; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Configuration; using BSN.Commons.Orm.Redis; +using BSN.Commons.Test.Data; +using Microsoft.Extensions.Options; +using Redis.OM; namespace BSN.Commons.Test.Infrastructure { diff --git a/Test/BSN.Commons.Orm.Redis.Tests/Mock/UserRepository.cs b/Test/BSN.Commons.Orm.Redis.Tests/Mock/UserRepository.cs index 57ecfde..b55f0be 100644 --- a/Test/BSN.Commons.Orm.Redis.Tests/Mock/UserRepository.cs +++ b/Test/BSN.Commons.Orm.Redis.Tests/Mock/UserRepository.cs @@ -1,5 +1,4 @@ using BSN.Commons.Infrastructure; -using BSN.Commons.Infrastructure.Redis; using BSN.Commons.Orm.Redis.Tests.Dto; namespace BSN.Commons.Orm.Redis.Tests.Mock From 754d6fd2a47f44de8752976d9350703df9bfd276 Mon Sep 17 00:00:00 2001 From: "m.rahimian" Date: Mon, 10 Aug 2026 13:14:05 +0330 Subject: [PATCH 02/11] fix redis test --- .../BSN.Commons.Orm.Redis.Tests.csproj | 1 + .../Infrastructure/DatabaseFactory.cs | 6 ++- .../RepositoryTest.cs | 37 ++++++++++++------- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/Test/BSN.Commons.Orm.Redis.Tests/BSN.Commons.Orm.Redis.Tests.csproj b/Test/BSN.Commons.Orm.Redis.Tests/BSN.Commons.Orm.Redis.Tests.csproj index c005acc..e5ac4bd 100644 --- a/Test/BSN.Commons.Orm.Redis.Tests/BSN.Commons.Orm.Redis.Tests.csproj +++ b/Test/BSN.Commons.Orm.Redis.Tests/BSN.Commons.Orm.Redis.Tests.csproj @@ -17,6 +17,7 @@ + diff --git a/Test/BSN.Commons.Orm.Redis.Tests/Infrastructure/DatabaseFactory.cs b/Test/BSN.Commons.Orm.Redis.Tests/Infrastructure/DatabaseFactory.cs index fabfd6e..82f9e0c 100644 --- a/Test/BSN.Commons.Orm.Redis.Tests/Infrastructure/DatabaseFactory.cs +++ b/Test/BSN.Commons.Orm.Redis.Tests/Infrastructure/DatabaseFactory.cs @@ -9,9 +9,11 @@ namespace BSN.Commons.Test.Infrastructure { internal class InMemoryDatabaseFactory : DatabaseFactory { - public InMemoryDatabaseFactory() : base(Options.Create(new RedisConnectionOptions + public InMemoryDatabaseFactory(RedisContainer _redis) : base(Options.Create(new RedisConnectionOptions { - ConnectionString = "redis://localhost:6379" + ConnectionString = $"redis://{_redis.GetConnectionString()}" + //var multiplexer = await ConnectionMultiplexer.ConnectAsync(connectionString); + //ConnectionString = "redis://localhost:6379" })) { diff --git a/Test/BSN.Commons.Orm.Redis.Tests/RepositoryTest.cs b/Test/BSN.Commons.Orm.Redis.Tests/RepositoryTest.cs index 3e24e4f..21c7cf7 100644 --- a/Test/BSN.Commons.Orm.Redis.Tests/RepositoryTest.cs +++ b/Test/BSN.Commons.Orm.Redis.Tests/RepositoryTest.cs @@ -1,24 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using BSN.Commons.Orm.Redis.Tests.Mock; +using BSN.Commons.Infrastructure; using BSN.Commons.Orm.Redis.Tests.Dto; -using BSN.Commons.Infrastructure; -using BSN.Commons.Infrastructure.Redis; -using Microsoft.Extensions.Options; -using NUnit.Framework; +using BSN.Commons.Orm.Redis.Tests.Mock; using BSN.Commons.Test.Infrastructure; +using NUnit.Framework; +using Testcontainers.Redis; namespace BSN.Commons.Orm.Redis.Tests { [TestFixture] public class RepositoryTest { + + [OneTimeSetUp] + public async Task OneTimeSetUp() + { + _redis = new RedisBuilder("redis/redis-stack-server:latest") + .Build(); + + await _redis.StartAsync(); + } + + [OneTimeTearDown] + public async Task OneTimeTearDown() + { + await _redis.DisposeAsync(); + } + [SetUp] public void SetUp() - { + { _databaseFactory = CreateDatabaseFactory(); _userRepository = CreateUserRepository(_databaseFactory); } @@ -31,7 +41,7 @@ public void TearDown() [Test] public void AddUserToDataBase_UserShouldBeCorrectlyAddedToDatabase() - { + { User user = new User() { FirstName = "Reza", @@ -48,7 +58,7 @@ public void AddUserToDataBase_UserShouldBeCorrectlyAddedToDatabase() public IDatabaseFactory CreateDatabaseFactory() { - return new InMemoryDatabaseFactory(); + return new InMemoryDatabaseFactory(_redis); } public IRepository CreateUserRepository(IDatabaseFactory databaseFactory) @@ -56,6 +66,7 @@ public IRepository CreateUserRepository(IDatabaseFactory databaseFactory) return new UserRepository(databaseFactory); } + private RedisContainer _redis = null!; protected IRepository _userRepository; protected IDatabaseFactory _databaseFactory; } From a2ec0b02745d871ab32270c17e0304406353fb5a Mon Sep 17 00:00:00 2001 From: "m.rahimian" Date: Mon, 10 Aug 2026 13:14:47 +0330 Subject: [PATCH 03/11] fix redis test --- .../Infrastructure/DatabaseFactory.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Test/BSN.Commons.Orm.Redis.Tests/Infrastructure/DatabaseFactory.cs b/Test/BSN.Commons.Orm.Redis.Tests/Infrastructure/DatabaseFactory.cs index 82f9e0c..7f057f9 100644 --- a/Test/BSN.Commons.Orm.Redis.Tests/Infrastructure/DatabaseFactory.cs +++ b/Test/BSN.Commons.Orm.Redis.Tests/Infrastructure/DatabaseFactory.cs @@ -4,6 +4,7 @@ using BSN.Commons.Test.Data; using Microsoft.Extensions.Options; using Redis.OM; +using Testcontainers.Redis; namespace BSN.Commons.Test.Infrastructure { @@ -11,9 +12,7 @@ internal class InMemoryDatabaseFactory : DatabaseFactory { public InMemoryDatabaseFactory(RedisContainer _redis) : base(Options.Create(new RedisConnectionOptions { - ConnectionString = $"redis://{_redis.GetConnectionString()}" - //var multiplexer = await ConnectionMultiplexer.ConnectAsync(connectionString); - //ConnectionString = "redis://localhost:6379" + ConnectionString = $"redis://{_redis.GetConnectionString()}" })) { From 8497777d1fbd6683d768af88ac7a5b9724a994ec Mon Sep 17 00:00:00 2001 From: "m.rahimian" Date: Mon, 10 Aug 2026 13:24:06 +0330 Subject: [PATCH 04/11] fix package versions --- .../BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj | 7 ++++--- .../Extensions/IServiceCollectionExtensions.cs | 5 +++-- .../BSN.Commons.Orm.EntityFrameworkCore.csproj | 2 ++ Source/BSN.Commons.Orm.Redis/BSN.Commons.Orm.Redis.csproj | 1 + Source/BSN.Commons/BSN.Commons.csproj | 4 ++-- Test/BSN.Commons.AutoMapper.Tests/AutoMapperTestBase.cs | 3 ++- .../BSN.Commons.AutoMapper.Tests.csproj | 2 +- .../CommonMapperProfileTests.cs | 7 ++++--- .../BSN.Commons.Orm.EntityFrameworkCore.Tests.csproj | 1 + 9 files changed, 20 insertions(+), 12 deletions(-) diff --git a/Source/BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj b/Source/BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj index 3e68171..5112740 100644 --- a/Source/BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj +++ b/Source/BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -49,8 +49,9 @@ - - + + + diff --git a/Source/BSN.Commons.AutoMapper/Extensions/IServiceCollectionExtensions.cs b/Source/BSN.Commons.AutoMapper/Extensions/IServiceCollectionExtensions.cs index edc2b4c..6699c98 100644 --- a/Source/BSN.Commons.AutoMapper/Extensions/IServiceCollectionExtensions.cs +++ b/Source/BSN.Commons.AutoMapper/Extensions/IServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using AutoMapper; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; namespace BSN.Commons.AutoMapper.Extensions { @@ -15,12 +16,12 @@ public static IServiceCollection AddCommonsAutoMapper(this IServiceCollection se configure(config); config.AddProfile(new CommonMapperProfile()); - }); + }, NullLoggerFactory.Instance); IMapper mapper = mappingConfig.CreateMapper(); services.AddSingleton(mapper); - + return services; } } diff --git a/Source/BSN.Commons.Orm.EntityFrameworkCore/BSN.Commons.Orm.EntityFrameworkCore.csproj b/Source/BSN.Commons.Orm.EntityFrameworkCore/BSN.Commons.Orm.EntityFrameworkCore.csproj index 05ff655..cf6117e 100644 --- a/Source/BSN.Commons.Orm.EntityFrameworkCore/BSN.Commons.Orm.EntityFrameworkCore.csproj +++ b/Source/BSN.Commons.Orm.EntityFrameworkCore/BSN.Commons.Orm.EntityFrameworkCore.csproj @@ -72,10 +72,12 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Source/BSN.Commons.Orm.Redis/BSN.Commons.Orm.Redis.csproj b/Source/BSN.Commons.Orm.Redis/BSN.Commons.Orm.Redis.csproj index f475a4a..a819f5b 100644 --- a/Source/BSN.Commons.Orm.Redis/BSN.Commons.Orm.Redis.csproj +++ b/Source/BSN.Commons.Orm.Redis/BSN.Commons.Orm.Redis.csproj @@ -74,6 +74,7 @@ + diff --git a/Source/BSN.Commons/BSN.Commons.csproj b/Source/BSN.Commons/BSN.Commons.csproj index 49a453c..79e51a8 100644 --- a/Source/BSN.Commons/BSN.Commons.csproj +++ b/Source/BSN.Commons/BSN.Commons.csproj @@ -53,8 +53,8 @@ - - + + diff --git a/Test/BSN.Commons.AutoMapper.Tests/AutoMapperTestBase.cs b/Test/BSN.Commons.AutoMapper.Tests/AutoMapperTestBase.cs index 6291ca6..9b72097 100644 --- a/Test/BSN.Commons.AutoMapper.Tests/AutoMapperTestBase.cs +++ b/Test/BSN.Commons.AutoMapper.Tests/AutoMapperTestBase.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Microsoft.Extensions.Logging.Abstractions; namespace BSN.Commons.AutoMapper.Tests { @@ -11,7 +12,7 @@ protected AutoMapperTestBase() var configuration = new MapperConfiguration(cfg => { cfg.AddProfile(); - }); + }, NullLoggerFactory.Instance); _mapper = configuration.CreateMapper(); } diff --git a/Test/BSN.Commons.AutoMapper.Tests/BSN.Commons.AutoMapper.Tests.csproj b/Test/BSN.Commons.AutoMapper.Tests/BSN.Commons.AutoMapper.Tests.csproj index 42189c4..ca6767e 100644 --- a/Test/BSN.Commons.AutoMapper.Tests/BSN.Commons.AutoMapper.Tests.csproj +++ b/Test/BSN.Commons.AutoMapper.Tests/BSN.Commons.AutoMapper.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/Test/BSN.Commons.AutoMapper.Tests/CommonMapperProfileTests.cs b/Test/BSN.Commons.AutoMapper.Tests/CommonMapperProfileTests.cs index 02ccf91..541ab14 100644 --- a/Test/BSN.Commons.AutoMapper.Tests/CommonMapperProfileTests.cs +++ b/Test/BSN.Commons.AutoMapper.Tests/CommonMapperProfileTests.cs @@ -1,5 +1,6 @@ using AutoMapper; using BSN.Commons.Responses; +using Microsoft.Extensions.Logging.Abstractions; namespace BSN.Commons.AutoMapper.Tests { @@ -11,7 +12,7 @@ public void PagedEntityCollectionToMetaDataConverter_ConvertsCorrectly() { // Arrange var profile = new CommonMapperProfile(); - var configuration = new MapperConfiguration(cfg => cfg.AddProfile(profile)); + var configuration = new MapperConfiguration(cfg => cfg.AddProfile(profile), NullLoggerFactory.Instance); var mapper = new Mapper(configuration); var pagedEntityCollection = new PagedEntityCollection { @@ -35,7 +36,7 @@ public void GenericIEnumerableToCollectionViewModelConverter_ConvertsCorrectly() { // Arrange var profile = new CommonMapperProfile(); - var configuration = new MapperConfiguration(cfg => cfg.AddProfile(profile)); + var configuration = new MapperConfiguration(cfg => cfg.AddProfile(profile), NullLoggerFactory.Instance); var mapper = new Mapper(configuration); var items = new List { 1, 2, 3 }; @@ -57,7 +58,7 @@ public void CustomProfileConverter_ConvertsCorrectly() { cfg.AddProfile(profile); cfg.AddProfile(customProfile); - }); + }, NullLoggerFactory.Instance); var mapper = new Mapper(configuration); var customEntity = new CustomEntity { Id = 1, Name = "Custom Entity" }; diff --git a/Test/BSN.Commons.Orm.EntityFrameworkCore.Tests/BSN.Commons.Orm.EntityFrameworkCore.Tests.csproj b/Test/BSN.Commons.Orm.EntityFrameworkCore.Tests/BSN.Commons.Orm.EntityFrameworkCore.Tests.csproj index 14a95fb..1b5403c 100644 --- a/Test/BSN.Commons.Orm.EntityFrameworkCore.Tests/BSN.Commons.Orm.EntityFrameworkCore.Tests.csproj +++ b/Test/BSN.Commons.Orm.EntityFrameworkCore.Tests/BSN.Commons.Orm.EntityFrameworkCore.Tests.csproj @@ -17,6 +17,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + From 91cc7ddc50eb3df2111101a798deabceacd40d6d Mon Sep 17 00:00:00 2001 From: "m.rahimian" Date: Mon, 10 Aug 2026 13:46:41 +0330 Subject: [PATCH 05/11] fix AutoMapper test --- .../IServiceCollectionExtensionsTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Test/BSN.Commons.AutoMapper.Tests/IServiceCollectionExtensionsTests.cs b/Test/BSN.Commons.AutoMapper.Tests/IServiceCollectionExtensionsTests.cs index 9f30ca4..a2b33b5 100644 --- a/Test/BSN.Commons.AutoMapper.Tests/IServiceCollectionExtensionsTests.cs +++ b/Test/BSN.Commons.AutoMapper.Tests/IServiceCollectionExtensionsTests.cs @@ -1,5 +1,7 @@ using AutoMapper; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace BSN.Commons.AutoMapper.Tests { @@ -11,7 +13,7 @@ public void AddAutoMapper_AddsMapperToServices() // Arrange var services = new ServiceCollection(); var configure = new Action(config => { }); - + services.AddSingleton(NullLoggerFactory.Instance); // Act services.AddAutoMapper(configure); var serviceProvider = services.BuildServiceProvider(); From 65ae060a0b2dd147b9b9fb4928af9270e2c67a4d Mon Sep 17 00:00:00 2001 From: "m.rahimian" Date: Sat, 15 Aug 2026 16:25:42 +0330 Subject: [PATCH 06/11] resolve some PR comments --- .../BSN.Commons.AutoMapper.csproj | 1 - .../RepositoryBase.cs | 29 ++--- .../RepositoryBase.cs | 110 +++++++++--------- .../BSN.Commons.Orm.Redis/RepositoryBase.cs | 25 +--- 4 files changed, 67 insertions(+), 98 deletions(-) diff --git a/Source/BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj b/Source/BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj index 5112740..8386aac 100644 --- a/Source/BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj +++ b/Source/BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj @@ -50,7 +50,6 @@ - diff --git a/Source/BSN.Commons.Orm.EntityFramework/RepositoryBase.cs b/Source/BSN.Commons.Orm.EntityFramework/RepositoryBase.cs index c0e3b1a..716326d 100644 --- a/Source/BSN.Commons.Orm.EntityFramework/RepositoryBase.cs +++ b/Source/BSN.Commons.Orm.EntityFramework/RepositoryBase.cs @@ -137,6 +137,12 @@ public virtual void UpdateRange( configurer(updateConfig); + if (updateConfig.AutoDetectChangedPropertiesEnabled) + { + _dataContext.Configuration.AutoDetectChangesEnabled = true; + return; + } + bool previousValue = _dataContext.Configuration.AutoDetectChangesEnabled; @@ -186,10 +192,7 @@ public virtual void Delete( { var objects = dbSet.Where(where); - foreach (T obj in objects) - { - dbSet.Remove(obj); - } + DeleteRange(objects); } @@ -309,37 +312,21 @@ public virtual async Task GetAsync( .FirstOrDefaultAsync(cancellationToken); } - /// /// Database Set /// protected readonly DbSet dbSet; - /// /// Database Context /// - protected DbContext DataContext - { - get - { - if (_dataContext == null) - { - _dataContext = - (DbContext)DatabaseFactory.Get(); - } - - return _dataContext; - } - } - + protected DbContext DataContext => _dataContext ?? (_dataContext = (DbContext)DatabaseFactory.Get()); /// /// Database Factory /// protected IDatabaseFactory DatabaseFactory { get; private set; } - private DbContext _dataContext; } } \ No newline at end of file diff --git a/Source/BSN.Commons.Orm.EntityFrameworkCore/RepositoryBase.cs b/Source/BSN.Commons.Orm.EntityFrameworkCore/RepositoryBase.cs index 67472fd..2a21070 100644 --- a/Source/BSN.Commons.Orm.EntityFrameworkCore/RepositoryBase.cs +++ b/Source/BSN.Commons.Orm.EntityFrameworkCore/RepositoryBase.cs @@ -13,11 +13,6 @@ namespace BSN.Commons.Orm.EntityFrameworkCore public class RepositoryBase : IRepository where T : class { - protected readonly DbSet dbSet; - - protected DbContext _dataContext; - - protected RepositoryBase(IDatabaseFactory databaseFactory) { if (databaseFactory == null) @@ -27,29 +22,14 @@ protected RepositoryBase(IDatabaseFactory databaseFactory) dbSet = DataContext.Set(); } - - protected DbContext DataContext - { - get - { - if (_dataContext == null) - _dataContext = (DbContext)DatabaseFactory.Get(); - - return _dataContext; - } - } - - - protected IDatabaseFactory DatabaseFactory { get; private set; } - - - public void Add(T entity) + /// + public virtual void Add(T entity) { dbSet.Add(entity); } - - public async Task AddAsync( + /// + public virtual async Task AddAsync( T entity, CancellationToken cancellationToken = default(CancellationToken)) { @@ -58,14 +38,14 @@ await dbSet .ConfigureAwait(false); } - - public void AddRange(IEnumerable entities) + /// + public virtual void AddRange(IEnumerable entities) { dbSet.AddRange(entities); } - - public Task AddRangeAsync( + /// + public virtual Task AddRangeAsync( IEnumerable entities, CancellationToken cancellationToken = default(CancellationToken)) { @@ -74,33 +54,33 @@ public Task AddRangeAsync( cancellationToken); } - - public void Delete(T entity) + /// + public virtual void Delete(T entity) { dbSet.Remove(entity); } - - public void Delete(Expression> where) + /// + public virtual void Delete(Expression> where) { - dbSet.RemoveRange( + DeleteRange( dbSet.Where(where)); } - - public void DeleteRange(IEnumerable entities) + /// + public virtual void DeleteRange(IEnumerable entities) { dbSet.RemoveRange(entities); } - + /// public virtual T GetById( KeyType id) { return dbSet.Find(id); } - + /// public virtual async Task GetByIdAsync( KeyType id, CancellationToken cancellationToken = default(CancellationToken)) @@ -111,7 +91,7 @@ public virtual async Task GetByIdAsync( .ConfigureAwait(false); } - + /// public virtual IEnumerable GetAll( bool asNoTracking = false) { @@ -123,7 +103,7 @@ public virtual IEnumerable GetAll( return query.ToList(); } - + /// public virtual async Task> GetAllAsync( bool asNoTracking = false, CancellationToken cancellationToken = default(CancellationToken)) @@ -138,7 +118,7 @@ public virtual async Task> GetAllAsync( .ConfigureAwait(false); } - + /// public virtual IEnumerable GetMany( Expression> where, bool asNoTracking = false) @@ -151,7 +131,7 @@ public virtual IEnumerable GetMany( return query; } - + /// public virtual async Task> GetManyAsync( Expression> where, bool asNoTracking = false, @@ -167,8 +147,8 @@ public virtual async Task> GetManyAsync( .ConfigureAwait(false); } - - public T Get( + /// + public virtual T Get( Expression> where, bool asNoTracking = false) { @@ -180,8 +160,8 @@ public T Get( return query.FirstOrDefault(); } - - public async Task GetAsync( + /// + public virtual async Task GetAsync( Expression> where, bool asNoTracking = false, CancellationToken cancellationToken = default(CancellationToken)) @@ -196,14 +176,14 @@ public async Task GetAsync( .ConfigureAwait(false); } - - public void Update(T entity) + /// + public virtual void Update(T entity) { Update(entity, cfg => cfg.IncludeAllProperties()); } - - public void Update( + /// + public virtual void Update( T entity, Action> configurer) { @@ -211,6 +191,7 @@ public void Update( configurer(updateConfig); + // TODO: Why this behaviour exist? if (updateConfig.AutoDetectChangedPropertiesEnabled) { _dataContext.ChangeTracker.AutoDetectChangesEnabled = true; @@ -249,16 +230,16 @@ public void Update( } } - - public void UpdateRange(IEnumerable entities) + /// + public virtual void UpdateRange(IEnumerable entities) { UpdateRange( entities, cfg => cfg.IncludeAllProperties()); } - - public void UpdateRange( + /// + public virtual void UpdateRange( IEnumerable entities, Action> configurer) { @@ -266,6 +247,12 @@ public void UpdateRange( configurer(updateConfig); + if (updateConfig.AutoDetectChangedPropertiesEnabled) + { + _dataContext.ChangeTracker.AutoDetectChangesEnabled = true; + return; + } + bool previous = _dataContext.ChangeTracker.AutoDetectChangesEnabled; @@ -300,5 +287,22 @@ public void UpdateRange( previous; } } + + /// + /// Database Set + /// + protected readonly DbSet dbSet; + + /// + /// Database Context + /// + protected DbContext DataContext => _dataContext ?? (_dataContext = (DbContext)DatabaseFactory.Get()); + + /// + /// Database Factory + /// + protected IDatabaseFactory DatabaseFactory { get; private set; } + + private DbContext _dataContext; } } \ No newline at end of file diff --git a/Source/BSN.Commons.Orm.Redis/RepositoryBase.cs b/Source/BSN.Commons.Orm.Redis/RepositoryBase.cs index 800033b..63cade9 100644 --- a/Source/BSN.Commons.Orm.Redis/RepositoryBase.cs +++ b/Source/BSN.Commons.Orm.Redis/RepositoryBase.cs @@ -271,32 +271,11 @@ public virtual Task> GetManyAsync( GetMany(where, asNoTracking)); } - - protected readonly IRedisCollection dbCollection; + protected IDatabaseFactory DatabaseFactory { get; private set; } - protected IDatabaseFactory DatabaseFactory - { - get; - private set; - } - - - protected IRedisConnectionProvider DataContext - { - get - { - if (_dataContext == null) - { - _dataContext = - (IRedisConnectionProvider)DatabaseFactory.Get(); - } - - return _dataContext; - } - } - + protected IRedisConnectionProvider DataContext => _dataContext ?? (_dataContext = (IRedisConnectionProvider)DatabaseFactory.Get()); private IRedisConnectionProvider _dataContext; } From 2e0dbfe917441299f1e18251d4f766f53bd56d67 Mon Sep 17 00:00:00 2001 From: "m.rahimian" Date: Sun, 16 Aug 2026 17:45:26 +0330 Subject: [PATCH 07/11] remove some pakages --- Source/BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj | 1 - .../BSN.Commons.Orm.EntityFrameworkCore.csproj | 1 - Source/BSN.Commons.Orm.Redis/BSN.Commons.Orm.Redis.csproj | 1 - 3 files changed, 3 deletions(-) diff --git a/Source/BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj b/Source/BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj index 6eeefff..0a9471c 100644 --- a/Source/BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj +++ b/Source/BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj @@ -50,7 +50,6 @@ - diff --git a/Source/BSN.Commons.Orm.EntityFrameworkCore/BSN.Commons.Orm.EntityFrameworkCore.csproj b/Source/BSN.Commons.Orm.EntityFrameworkCore/BSN.Commons.Orm.EntityFrameworkCore.csproj index b61fb85..f5ab255 100644 --- a/Source/BSN.Commons.Orm.EntityFrameworkCore/BSN.Commons.Orm.EntityFrameworkCore.csproj +++ b/Source/BSN.Commons.Orm.EntityFrameworkCore/BSN.Commons.Orm.EntityFrameworkCore.csproj @@ -77,7 +77,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/Source/BSN.Commons.Orm.Redis/BSN.Commons.Orm.Redis.csproj b/Source/BSN.Commons.Orm.Redis/BSN.Commons.Orm.Redis.csproj index d667fe6..5ebcdad 100644 --- a/Source/BSN.Commons.Orm.Redis/BSN.Commons.Orm.Redis.csproj +++ b/Source/BSN.Commons.Orm.Redis/BSN.Commons.Orm.Redis.csproj @@ -74,7 +74,6 @@ - From 496625916921773db1d41f1097ccd593a9613fdc Mon Sep 17 00:00:00 2001 From: "m.rahimian" Date: Sun, 16 Aug 2026 17:49:59 +0330 Subject: [PATCH 08/11] remove package --- .../BSN.Commons.Orm.EntityFrameworkCore.csproj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Source/BSN.Commons.Orm.EntityFrameworkCore/BSN.Commons.Orm.EntityFrameworkCore.csproj b/Source/BSN.Commons.Orm.EntityFrameworkCore/BSN.Commons.Orm.EntityFrameworkCore.csproj index f5ab255..051ccd5 100644 --- a/Source/BSN.Commons.Orm.EntityFrameworkCore/BSN.Commons.Orm.EntityFrameworkCore.csproj +++ b/Source/BSN.Commons.Orm.EntityFrameworkCore/BSN.Commons.Orm.EntityFrameworkCore.csproj @@ -1,4 +1,4 @@ - + net6.0;net8.0 @@ -72,7 +72,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive From 812392e87baebda7edd9c4461ee858e48b78ce40 Mon Sep 17 00:00:00 2001 From: "m.rahimian" Date: Wed, 26 Aug 2026 16:25:42 +0330 Subject: [PATCH 09/11] 1-Roleback some package versions 2-Segregate async interfaces 3-Add async tests --- .../BSN.Commons.AutoMapper.csproj | 3 +- .../IServiceCollectionExtensions.cs | 3 +- .../BSN.Commons.Orm.EntityFramework.csproj | 1 + .../DatabaseFactory.cs | 29 ++ .../RepositoryBase.cs | 3 +- .../DynamicFilterableRepositoryBase.cs | 74 +++- .../Extensions/IQueryableExtensions.cs | 5 +- .../RepositoryBase.cs | 34 +- .../BSN.Commons.Orm.Redis/DatabaseFactory.cs | 20 +- Source/BSN.Commons.Orm.Redis/DbContext.cs | 2 +- .../BSN.Commons.Orm.Redis/RepositoryBase.cs | 83 ++-- .../BSN.Commons/Infrastructure/Disposable.cs | 80 ++-- .../BSN.Commons/Infrastructure/EnlistTask.cs | 124 ++++-- .../Infrastructure/IAsyncDbContext.cs | 18 + .../Infrastructure/IAsyncRepository.cs | 64 +++ .../Infrastructure/IAsyncUnitOfWork.cs | 19 + .../Infrastructure/IDatabaseFactory.cs | 5 + .../BSN.Commons/Infrastructure/IDbContext.cs | 12 +- .../IDynamicFilterableAsyncRepository.cs | 70 +++ .../BSN.Commons/Infrastructure/IRepository.cs | 55 +-- .../BSN.Commons/Infrastructure/IUnitOfWork.cs | 5 +- .../BSN.Commons/Infrastructure/UnitOfWork.cs | 275 +++++++----- .../Data/UnitTestContext.cs | 15 +- .../Infrastructure/DatabaseFactory.cs | 12 + .../Mock/UsersRepsitory.cs | 2 +- .../UnitOfWorkTest.cs | 5 + .../Data/UnitTestContext.cs | 16 +- .../Infrastructure/DatabaseFactory.cs | 20 +- .../UnitOfWorkTest.cs | 5 + .../Data/UnitTestContext.cs | 22 +- .../UnitOfWorkArrangementsAbstractFactory.cs | 1 + Test/BSN.Commons.Tests/UnitOfWorkTestBase.cs | 419 +++++++++++++++++- 32 files changed, 1146 insertions(+), 355 deletions(-) create mode 100644 Source/BSN.Commons.Orm.EntityFramework/DatabaseFactory.cs create mode 100644 Source/BSN.Commons/Infrastructure/IAsyncDbContext.cs create mode 100644 Source/BSN.Commons/Infrastructure/IAsyncRepository.cs create mode 100644 Source/BSN.Commons/Infrastructure/IAsyncUnitOfWork.cs create mode 100644 Source/BSN.Commons/Infrastructure/IDynamicFilterableAsyncRepository.cs diff --git a/Source/BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj b/Source/BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj index 0a9471c..8a5571f 100644 --- a/Source/BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj +++ b/Source/BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj @@ -49,7 +49,8 @@ - + + diff --git a/Source/BSN.Commons.AutoMapper/Extensions/IServiceCollectionExtensions.cs b/Source/BSN.Commons.AutoMapper/Extensions/IServiceCollectionExtensions.cs index 6699c98..83eb153 100644 --- a/Source/BSN.Commons.AutoMapper/Extensions/IServiceCollectionExtensions.cs +++ b/Source/BSN.Commons.AutoMapper/Extensions/IServiceCollectionExtensions.cs @@ -1,6 +1,5 @@ using AutoMapper; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging.Abstractions; namespace BSN.Commons.AutoMapper.Extensions { @@ -16,7 +15,7 @@ public static IServiceCollection AddCommonsAutoMapper(this IServiceCollection se configure(config); config.AddProfile(new CommonMapperProfile()); - }, NullLoggerFactory.Instance); + }); IMapper mapper = mappingConfig.CreateMapper(); diff --git a/Source/BSN.Commons.Orm.EntityFramework/BSN.Commons.Orm.EntityFramework.csproj b/Source/BSN.Commons.Orm.EntityFramework/BSN.Commons.Orm.EntityFramework.csproj index 03fcba4..1922231 100644 --- a/Source/BSN.Commons.Orm.EntityFramework/BSN.Commons.Orm.EntityFramework.csproj +++ b/Source/BSN.Commons.Orm.EntityFramework/BSN.Commons.Orm.EntityFramework.csproj @@ -63,6 +63,7 @@ + diff --git a/Source/BSN.Commons.Orm.EntityFramework/DatabaseFactory.cs b/Source/BSN.Commons.Orm.EntityFramework/DatabaseFactory.cs new file mode 100644 index 0000000..b0530c1 --- /dev/null +++ b/Source/BSN.Commons.Orm.EntityFramework/DatabaseFactory.cs @@ -0,0 +1,29 @@ +using BSN.Commons.Infrastructure; +using System.Data.Entity; + +namespace BSN.Commons.Orm.EntityFramework +{ + public class DatabaseFactory : Disposable, IDatabaseFactory where TContext : DbContext, IDbContext, IAsyncDbContext + { + public DatabaseFactory(TContext context) + { + _dataContext = context; + } + + public IDbContext Get() + { + return _dataContext; + } + + public void Dispose() + { + _dataContext?.Dispose(); + } + + public IAsyncDbContext GetAsyncContext() + { + return _dataContext; + } + protected TContext _dataContext; + } +} \ No newline at end of file diff --git a/Source/BSN.Commons.Orm.EntityFramework/RepositoryBase.cs b/Source/BSN.Commons.Orm.EntityFramework/RepositoryBase.cs index 716326d..8dd4b79 100644 --- a/Source/BSN.Commons.Orm.EntityFramework/RepositoryBase.cs +++ b/Source/BSN.Commons.Orm.EntityFramework/RepositoryBase.cs @@ -9,8 +9,9 @@ namespace BSN.Commons.Orm.EntityFramework { + /// - public abstract partial class RepositoryBase : IRepository + public abstract partial class RepositoryBase : IRepository, IAsyncRepository where T : class { /// diff --git a/Source/BSN.Commons.Orm.EntityFrameworkCore/DynamicFilterableRepositoryBase.cs b/Source/BSN.Commons.Orm.EntityFrameworkCore/DynamicFilterableRepositoryBase.cs index add12e8..eb9ca6c 100644 --- a/Source/BSN.Commons.Orm.EntityFrameworkCore/DynamicFilterableRepositoryBase.cs +++ b/Source/BSN.Commons.Orm.EntityFrameworkCore/DynamicFilterableRepositoryBase.cs @@ -1,20 +1,23 @@ using BSN.Commons.Extensions; using BSN.Commons.Infrastructure; using BSN.Commons.Orm.EntityFrameworkCore.Extensions; +using Microsoft.EntityFrameworkCore; using Sieve.Exceptions; using Sieve.Models; using Sieve.Services; using System; using System.Linq; using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; namespace BSN.Commons.Orm.EntityFrameworkCore { /// - /// Default implementation of + /// Default implementation of /// /// - public class DynamicFilterableRepositoryBase : RepositoryBase, IDynamicFilterableRepository where T : class + public class DynamicFilterableRepositoryBase : RepositoryBase, IDynamicFilterableRepository, IDynamicFilterableAsyncRepository where T : class { /// protected DynamicFilterableRepositoryBase(IDatabaseFactory databaseFactory, ISieveProcessor sieveProcessor) : base(databaseFactory) @@ -51,6 +54,72 @@ public PagedEntityCollection GetMany(string filters, string sorts, uint pageN return GetMany((entity) => true, filters, sorts, pageNumber, pageSize); } + /// + public async Task> GetManyAsync( + Expression> where, + string filters, + string sorts, + uint pageNumber, + uint pageSize, + CancellationToken cancellationToken = default) + { + if (pageNumber == 0) + throw new ArgumentException( + "Must be greater than zero.", + nameof(pageNumber)); + + if (pageSize == 0) + throw new ArgumentException( + "Must be greater than zero.", + nameof(pageSize)); + + ArgumentNullException.ThrowIfNull(where); + + IQueryable query = dbSet.Where(where); + + try + { + query = SieveProcessor.Apply( + new SieveModel + { + Filters = filters, + Sorts = sorts, + PageSize = (int)pageSize, + Page = (int)pageNumber + }, + query, + applyPagination: false); + } + catch (SieveException ex) + { + throw new InvalidOperationException( + ex.ExtractMessage(), + ex); + } + + return await query.PaginateAsync( + pageNumber, + pageSize, + cancellationToken); + } + + /// + public Task> GetManyAsync( + string filters, + string sorts, + uint pageNumber, + uint pageSize, + CancellationToken cancellationToken = default) + { + return GetManyAsync( + entity => true, + filters, + sorts, + pageNumber, + pageSize, + cancellationToken); + } + /// /// The engine of filtering /// @@ -58,5 +127,6 @@ public PagedEntityCollection GetMany(string filters, string sorts, uint pageN /// So if you want to any extra filtering or changing behaviour of current filtering, you have to use this engine /// protected ISieveProcessor SieveProcessor { get; } + } } \ No newline at end of file diff --git a/Source/BSN.Commons.Orm.EntityFrameworkCore/Extensions/IQueryableExtensions.cs b/Source/BSN.Commons.Orm.EntityFrameworkCore/Extensions/IQueryableExtensions.cs index e019de8..685ee0e 100644 --- a/Source/BSN.Commons.Orm.EntityFrameworkCore/Extensions/IQueryableExtensions.cs +++ b/Source/BSN.Commons.Orm.EntityFrameworkCore/Extensions/IQueryableExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace BSN.Commons.Extensions @@ -39,7 +40,7 @@ public static PagedEntityCollection Paginate(this IQueryable query, uin /// Paginate IQueryable of /// with given pageNumber and pageSize /// - public static async Task> PaginateAsync(this IQueryable query, uint pageNumber, uint pageSize) + public static async Task> PaginateAsync(this IQueryable query, uint pageNumber, uint pageSize,CancellationToken cancellationToken = default) { if (pageNumber <= 0) throw new ArgumentException("Must be greater than zero.", nameof(pageNumber)); @@ -52,7 +53,7 @@ public static async Task> PaginateAsync(this IQuerya CurrentPage = pageNumber, PageSize = pageSize, RecordCount = (uint) await query.CountAsync(), - Results = await query.Skip((int)((pageNumber - 1) * pageSize)).Take((int)pageSize).ToListAsync() + Results = await query.Skip((int)((pageNumber - 1) * pageSize)).Take((int)pageSize).ToListAsync(cancellationToken) }; result.PageCount = (uint)Math.Ceiling((double)result.RecordCount / pageSize); diff --git a/Source/BSN.Commons.Orm.EntityFrameworkCore/RepositoryBase.cs b/Source/BSN.Commons.Orm.EntityFrameworkCore/RepositoryBase.cs index 2a21070..69ce719 100644 --- a/Source/BSN.Commons.Orm.EntityFrameworkCore/RepositoryBase.cs +++ b/Source/BSN.Commons.Orm.EntityFrameworkCore/RepositoryBase.cs @@ -9,8 +9,9 @@ namespace BSN.Commons.Orm.EntityFrameworkCore { + /// - public class RepositoryBase : IRepository + public class RepositoryBase : IRepository, IAsyncRepository where T : class { protected RepositoryBase(IDatabaseFactory databaseFactory) @@ -28,10 +29,10 @@ public virtual void Add(T entity) dbSet.Add(entity); } - /// + /// public virtual async Task AddAsync( T entity, - CancellationToken cancellationToken = default(CancellationToken)) + CancellationToken cancellationToken = default) { await dbSet .AddAsync(entity, cancellationToken) @@ -47,7 +48,7 @@ public virtual void AddRange(IEnumerable entities) /// public virtual Task AddRangeAsync( IEnumerable entities, - CancellationToken cancellationToken = default(CancellationToken)) + CancellationToken cancellationToken = default) { return dbSet.AddRangeAsync( entities, @@ -83,12 +84,12 @@ public virtual T GetById( /// public virtual async Task GetByIdAsync( KeyType id, - CancellationToken cancellationToken = default(CancellationToken)) + CancellationToken cancellationToken = default) { - return await dbSet - .FindAsync(id , - cancellationToken) - .ConfigureAwait(false); + return await dbSet + .FindAsync(id, + cancellationToken) + .ConfigureAwait(false); } /// @@ -106,7 +107,7 @@ public virtual IEnumerable GetAll( /// public virtual async Task> GetAllAsync( bool asNoTracking = false, - CancellationToken cancellationToken = default(CancellationToken)) + CancellationToken cancellationToken = default) { IQueryable query = dbSet; @@ -114,8 +115,7 @@ public virtual async Task> GetAllAsync( query = query.AsNoTracking(); return await query - .ToListAsync(cancellationToken) - .ConfigureAwait(false); + .ToListAsync(cancellationToken); } /// @@ -135,7 +135,7 @@ public virtual IEnumerable GetMany( public virtual async Task> GetManyAsync( Expression> where, bool asNoTracking = false, - CancellationToken cancellationToken = default(CancellationToken)) + CancellationToken cancellationToken = default) { IQueryable query = dbSet.Where(where); @@ -143,8 +143,7 @@ public virtual async Task> GetManyAsync( query = query.AsNoTracking(); return await query - .ToListAsync(cancellationToken) - .ConfigureAwait(false); + .ToListAsync(cancellationToken); } /// @@ -164,7 +163,7 @@ public virtual T Get( public virtual async Task GetAsync( Expression> where, bool asNoTracking = false, - CancellationToken cancellationToken = default(CancellationToken)) + CancellationToken cancellationToken = default) { IQueryable query = dbSet.Where(where); @@ -172,8 +171,7 @@ public virtual async Task GetAsync( query = query.AsNoTracking(); return await query - .FirstOrDefaultAsync(cancellationToken) - .ConfigureAwait(false); + .FirstOrDefaultAsync(cancellationToken); } /// diff --git a/Source/BSN.Commons.Orm.Redis/DatabaseFactory.cs b/Source/BSN.Commons.Orm.Redis/DatabaseFactory.cs index 51dda91..e431913 100644 --- a/Source/BSN.Commons.Orm.Redis/DatabaseFactory.cs +++ b/Source/BSN.Commons.Orm.Redis/DatabaseFactory.cs @@ -1,20 +1,14 @@ -using Microsoft.Extensions.Options; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using BSN.Commons.Infrastructure; +using BSN.Commons.Infrastructure; using BSN.Commons.Infrastructure.Redis; -using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; namespace BSN.Commons.Orm.Redis { /// /// Database Factory for Redis /// - public class DatabaseFactory : Disposable, IDatabaseFactory where TDbContext : Redis.DbContext, ICreatable, TDbContext> + public class DatabaseFactory : Disposable, IDatabaseFactory where TDbContext : DbContext, ICreatable, TDbContext> { /// /// Constructor of Redis Database Factory @@ -39,6 +33,11 @@ public IDbContext Get() { return _redisDbContext ?? (_redisDbContext = TDbContext.Create(RedisConnectionOptions)); } + /// + public IAsyncDbContext GetAsyncContext() + { + return _asyncRedisDbContext ?? (_asyncRedisDbContext = TDbContext.Create(RedisConnectionOptions)); + } /// /// Redis Connection Options @@ -47,5 +46,6 @@ public IDbContext Get() private readonly IOptions redisConnectionOptions; private IDbContext _redisDbContext; + private IAsyncDbContext _asyncRedisDbContext; } } diff --git a/Source/BSN.Commons.Orm.Redis/DbContext.cs b/Source/BSN.Commons.Orm.Redis/DbContext.cs index e215ed0..686b1c5 100644 --- a/Source/BSN.Commons.Orm.Redis/DbContext.cs +++ b/Source/BSN.Commons.Orm.Redis/DbContext.cs @@ -10,7 +10,7 @@ namespace BSN.Commons.Orm.Redis /// /// Redis Database Context /// - public class DbContext : RedisConnectionProvider, IDbContext, ICreatable, DbContext> + public class DbContext : RedisConnectionProvider, IDbContext, IAsyncDbContext, ICreatable, DbContext> { /// /// Constructor of Redis Database Context diff --git a/Source/BSN.Commons.Orm.Redis/RepositoryBase.cs b/Source/BSN.Commons.Orm.Redis/RepositoryBase.cs index 63cade9..94e77af 100644 --- a/Source/BSN.Commons.Orm.Redis/RepositoryBase.cs +++ b/Source/BSN.Commons.Orm.Redis/RepositoryBase.cs @@ -15,7 +15,7 @@ namespace BSN.Commons.Orm.Redis /// Repository Base for Redis Implementation /// /// - public class RepositoryBase : IRepository + public class RepositoryBase : IRepository, IAsyncRepository where T : class { /// @@ -46,13 +46,13 @@ public virtual void Add(T entity) /// public virtual Task AddAsync( T entity, - CancellationToken cancellationToken = default(CancellationToken)) + CancellationToken cancellationToken = default) { + if (entity == null) + throw new ArgumentNullException(nameof(entity)); cancellationToken.ThrowIfCancellationRequested(); - Add(entity); - - return Task.CompletedTask; + return dbCollection.InsertAsync(entity); } @@ -73,13 +73,10 @@ public virtual void AddRange( /// public virtual Task AddRangeAsync( IEnumerable entities, - CancellationToken cancellationToken = default(CancellationToken)) + CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - - AddRange(entities); - - return Task.CompletedTask; + return dbCollection.InsertAsync(entities); } @@ -189,16 +186,31 @@ public virtual T GetById( /// - public virtual Task GetByIdAsync( + public virtual async Task GetByIdAsync( KeyType id, - CancellationToken cancellationToken = default(CancellationToken)) + CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); + if (id == null) + throw new ArgumentNullException(nameof(id)); - return Task.FromResult( - GetById(id)); - } + if (id is string) + { + var entity = await dbCollection.FindByIdAsync(id.ToString()); + if (entity == null) + { + throw new KeyNotFoundException( + $"Entity with key {id} was not found."); + } + + return entity; + } + + throw new NotSupportedException( + $"Redis repository does not support key type {typeof(KeyType)}."); + + } public virtual T Get( Expression> where, @@ -208,21 +220,22 @@ public virtual T Get( throw new ArgumentNullException(nameof(where)); return dbCollection - .Where(where) - .FirstOrDefault(); + .FirstOrDefault(where); } - /// public virtual Task GetAsync( Expression> where, bool asNoTracking = false, - CancellationToken cancellationToken = default(CancellationToken)) + CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - return Task.FromResult( - Get(where, asNoTracking)); + if (where == null) + throw new ArgumentNullException(nameof(where)); + + return dbCollection + .FirstOrDefaultAsync(where); } @@ -231,19 +244,23 @@ public virtual IEnumerable GetAll( bool asNoTracking = false) { return dbCollection - .Where(x => true); + .Where(entity => true) + .AsEnumerable(); } /// - public virtual Task> GetAllAsync( + public virtual async Task> GetAllAsync( bool asNoTracking = false, - CancellationToken cancellationToken = default(CancellationToken)) + CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - return Task.FromResult( - GetAll(asNoTracking)); + var result = await dbCollection + .Where(entity => true) + .ToListAsync(); + + return result.AsEnumerable(); } @@ -260,15 +277,21 @@ public virtual IEnumerable GetMany( /// - public virtual Task> GetManyAsync( + public virtual async Task> GetManyAsync( Expression> where, bool asNoTracking = false, - CancellationToken cancellationToken = default(CancellationToken)) + CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - return Task.FromResult( - GetMany(where, asNoTracking)); + if (where == null) + throw new ArgumentNullException(nameof(where)); + + var result = await dbCollection + .Where(where) + .ToListAsync(); + + return result.AsEnumerable(); } protected readonly IRedisCollection dbCollection; diff --git a/Source/BSN.Commons/Infrastructure/Disposable.cs b/Source/BSN.Commons/Infrastructure/Disposable.cs index 3515544..30a3f6a 100644 --- a/Source/BSN.Commons/Infrastructure/Disposable.cs +++ b/Source/BSN.Commons/Infrastructure/Disposable.cs @@ -1,34 +1,56 @@ using System; +using System.Threading; namespace BSN.Commons.Infrastructure { - public class Disposable : IDisposable - { - private bool _isDisposed; - - - ~Disposable() - { - Dispose(false); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - - private void Dispose(bool disposing) - { - if (!_isDisposed && disposing) - { - DisposeCore(); - } - - _isDisposed = true; - } - - protected virtual void DisposeCore() { } - } + /// + /// Provides a base implementation of the standard dispose pattern. + /// + public abstract class Disposable : IDisposable + { + private int _disposed; + + ~Disposable() + { + Dispose(false); + } + + /// + /// Gets a value indicating whether this instance has been disposed. + /// + protected bool IsDisposed => + Volatile.Read(ref _disposed) != 0; + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases resources used by this instance. + /// + /// + /// true when called from ; + /// false when called from the finalizer. + /// + protected virtual void Dispose(bool disposing) + { + if (Interlocked.Exchange(ref _disposed, 1) != 0) + return; + + if (disposing) + { + DisposeCore(); + } + } + + /// + /// Releases managed resources. + /// + protected virtual void DisposeCore() + { + } + } } \ No newline at end of file diff --git a/Source/BSN.Commons/Infrastructure/EnlistTask.cs b/Source/BSN.Commons/Infrastructure/EnlistTask.cs index e26977b..c9f2861 100644 --- a/Source/BSN.Commons/Infrastructure/EnlistTask.cs +++ b/Source/BSN.Commons/Infrastructure/EnlistTask.cs @@ -4,70 +4,140 @@ namespace BSN.Commons.Infrastructure { + public class EnlistTask : ITaskUnit { - private readonly Task _executTask; + private readonly Task _executeTask; private readonly Task _rollbackTask; public Exception Exception { get; set; } - public EnlistTask(Task execut, Task rollback) + public EnlistTask(Task execute, Task rollback) { - _executTask = execut; - _rollbackTask = rollback; + _executeTask = execute + ?? throw new ArgumentNullException(nameof(execute)); + + _rollbackTask = rollback + ?? throw new ArgumentNullException(nameof(rollback)); } public Task Execute() { - _executTask.RunSynchronously(); + try + { + _executeTask.RunSynchronously(); + + if (_executeTask.IsFaulted) + { + Exception = _executeTask.Exception?.InnerException + ?? new Exception("Execute task faulted."); - if (_executTask.Status == TaskStatus.Faulted) - Exception = _executTask.Exception ?? new Exception("Execute Faulted"); + throw Exception; + } - if (Exception != null) - throw Exception; + if (_executeTask.IsCanceled) + { + Exception = new TaskCanceledException(_executeTask); + + throw Exception; + } + + return _executeTask; + } + catch (Exception ex) + { + Exception = Exception ?? ex; - return _executTask; + throw; + } } public Task Rollback() { - _rollbackTask.RunSynchronously(); + try + { + _rollbackTask.RunSynchronously(); + + if (_rollbackTask.IsFaulted) + { + var rollbackException = + _rollbackTask.Exception?.InnerException + ?? new Exception("Rollback task faulted."); + + Exception = Exception ?? rollbackException; - if (_executTask.Status == TaskStatus.Faulted) - Exception = _executTask.Exception ?? new Exception("Rollback Faulted"); + throw rollbackException; + } - return _rollbackTask; + if (_rollbackTask.IsCanceled) + { + var rollbackException = + new TaskCanceledException(_rollbackTask); + + Exception = Exception ?? rollbackException; + + throw rollbackException; + } + + return _rollbackTask; + } + catch (Exception ex) + { + Exception = Exception ?? ex; + throw; + } } - public void Commit(Enlistment enlistment) + public void Prepare( + PreparingEnlistment preparingEnlistment) { - enlistment.Done(); + try + { + Execute(); + + preparingEnlistment.Prepared(); + } + catch (Exception ex) + { + Exception = Exception ?? ex; + + try + { + Rollback(); + } + catch + { + // The original transaction failure must remain + // the failure reported to System.Transactions. + } + + preparingEnlistment.ForceRollback(Exception); + } } - public void InDoubt(Enlistment enlistment) + public void Commit( + Enlistment enlistment) { enlistment.Done(); } - public void Prepare(PreparingEnlistment preparingEnlistment) + public void Rollback( + Enlistment enlistment) { try { - Execute(); - preparingEnlistment.Prepared(); + Rollback(); } - catch (Exception ex) + finally { - Exception = ex; - Rollback(); - preparingEnlistment.ForceRollback(ex); + enlistment.Done(); } } - public void Rollback(Enlistment enlistment) + public void InDoubt( + Enlistment enlistment) { - Rollback(); + enlistment.Done(); } } -} +} \ No newline at end of file diff --git a/Source/BSN.Commons/Infrastructure/IAsyncDbContext.cs b/Source/BSN.Commons/Infrastructure/IAsyncDbContext.cs new file mode 100644 index 0000000..60a7579 --- /dev/null +++ b/Source/BSN.Commons/Infrastructure/IAsyncDbContext.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace BSN.Commons.Infrastructure +{ + /// + /// Async Interface for Database Context + /// + public interface IAsyncDbContext + { + /// + /// Save changes to the database asynchronously. + /// + Task SaveChangesAsync( + CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/Source/BSN.Commons/Infrastructure/IAsyncRepository.cs b/Source/BSN.Commons/Infrastructure/IAsyncRepository.cs new file mode 100644 index 0000000..17b4323 --- /dev/null +++ b/Source/BSN.Commons/Infrastructure/IAsyncRepository.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; + +namespace BSN.Commons.Infrastructure +{ + /// + /// Async Repository Pattern Interface for abstract communicating with DataBase + /// + /// + public interface IAsyncRepository where T : class + { + /// + /// Add new object asynchronously. + /// + Task AddAsync( + T entity, + CancellationToken cancellationToken = default); + + + /// + /// Add a range of objects asynchronously. + /// + Task AddRangeAsync( + IEnumerable entities, + CancellationToken cancellationToken = default); + + + /// + /// Get object by identifier asynchronously. + /// + Task GetByIdAsync( + KeyType id, + CancellationToken cancellationToken = default); + + + /// + /// Get object using expression asynchronously. + /// + Task GetAsync( + Expression> where, + bool asNoTracking = false, + CancellationToken cancellationToken = default); + + + /// + /// Get all objects asynchronously. + /// + Task> GetAllAsync( + bool asNoTracking = false, + CancellationToken cancellationToken = default); + + + /// + /// Get objects using expression asynchronously. + /// + Task> GetManyAsync( + Expression> where, + bool asNoTracking = false, + CancellationToken cancellationToken = default); + } +} diff --git a/Source/BSN.Commons/Infrastructure/IAsyncUnitOfWork.cs b/Source/BSN.Commons/Infrastructure/IAsyncUnitOfWork.cs new file mode 100644 index 0000000..c118a60 --- /dev/null +++ b/Source/BSN.Commons/Infrastructure/IAsyncUnitOfWork.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace BSN.Commons.Infrastructure +{ + public interface IAsyncUnitOfWork + { + IDatabaseFactory DatabaseFactory { get; } + + IReadOnlyCollection Exceptions { get; } + + void AddToQueue(ITaskUnit task); + + Task CommitAsync( + CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/Source/BSN.Commons/Infrastructure/IDatabaseFactory.cs b/Source/BSN.Commons/Infrastructure/IDatabaseFactory.cs index 7fc4a5c..3b61c37 100644 --- a/Source/BSN.Commons/Infrastructure/IDatabaseFactory.cs +++ b/Source/BSN.Commons/Infrastructure/IDatabaseFactory.cs @@ -12,5 +12,10 @@ public interface IDatabaseFactory : IDisposable /// /// IDbContext Get(); + /// + /// Get the database context for asynchronous operations + /// + /// + IAsyncDbContext GetAsyncContext(); } } diff --git a/Source/BSN.Commons/Infrastructure/IDbContext.cs b/Source/BSN.Commons/Infrastructure/IDbContext.cs index dcc8c76..26b0e3d 100644 --- a/Source/BSN.Commons/Infrastructure/IDbContext.cs +++ b/Source/BSN.Commons/Infrastructure/IDbContext.cs @@ -1,23 +1,15 @@ using System; -using System.Threading; -using System.Threading.Tasks; namespace BSN.Commons.Infrastructure { /// /// Interface for Database Context /// - public interface IDbContext : IDisposable, IAsyncDisposable + public interface IDbContext : IDisposable { /// /// Save changes to the database. /// - int SaveChanges(); - - /// - /// Save changes to the database asynchronously. - /// - Task SaveChangesAsync( - CancellationToken cancellationToken = default); + int SaveChanges(); } } \ No newline at end of file diff --git a/Source/BSN.Commons/Infrastructure/IDynamicFilterableAsyncRepository.cs b/Source/BSN.Commons/Infrastructure/IDynamicFilterableAsyncRepository.cs new file mode 100644 index 0000000..547db69 --- /dev/null +++ b/Source/BSN.Commons/Infrastructure/IDynamicFilterableAsyncRepository.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; + +namespace BSN.Commons.Infrastructure +{ + /// + /// Adds dynamic filterable query capabilities to the + /// pattern. + /// + /// The entity type. + public interface IDynamicFilterableAsyncRepository : IAsyncRepository + where T : class + { + /// + /// Asynchronously gets a paginated list of objects based on . + /// + /// + /// This method retrieves objects based on filters and applies pagination. + /// To retrieve all objects that match the filters, you need to iterate through all pages. + /// + /// + /// A comma-delimited list of {Name}{Operator}{Value}. + /// + /// + /// A comma-delimited ordered list of property names to sort by. + /// Adding a - before the name switches to descending order. + /// + /// The number of the page to return. + /// The number of items returned per page. + /// + /// A token to cancel the asynchronous operation. + /// + /// + /// A task containing a paginated list of matched objects. + /// + Task> GetManyAsync( + string filters, + string sorts, + uint pageNumber, + uint pageSize, + CancellationToken cancellationToken = default); + + /// + /// Asynchronously gets a paginated list of objects based on + /// within the scope of . + /// + /// A function to test each element for a condition. + /// A comma-delimited list of dynamic filters. + /// + /// A comma-delimited ordered list of property names to sort by. + /// + /// The number of the page to return. + /// The number of items returned per page. + /// + /// A token to cancel the asynchronous operation. + /// + /// + /// A task containing a paginated list of matched objects. + /// + Task> GetManyAsync( + Expression> where, + string filters, + string sorts, + uint pageNumber, + uint pageSize, + CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/Source/BSN.Commons/Infrastructure/IRepository.cs b/Source/BSN.Commons/Infrastructure/IRepository.cs index 29ae691..efa5b20 100644 --- a/Source/BSN.Commons/Infrastructure/IRepository.cs +++ b/Source/BSN.Commons/Infrastructure/IRepository.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; namespace BSN.Commons.Infrastructure { @@ -10,8 +8,8 @@ namespace BSN.Commons.Infrastructure /// Repository Pattern Interface for abstract communicating with DataBase /// /// - public interface IRepository where T : class - { + public interface IRepository where T : class + { /// /// Add new object to repository. /// @@ -96,54 +94,5 @@ public interface IRepository where T : class /// No Tracking /// List of Objects IEnumerable GetMany(Expression> where, bool asNoTracking = false); - - /// - /// Add new object asynchronously. - /// - Task AddAsync( - T entity, - CancellationToken cancellationToken = default(CancellationToken)); - - - /// - /// Add a range of objects asynchronously. - /// - Task AddRangeAsync( - IEnumerable entities, - CancellationToken cancellationToken = default(CancellationToken)); - - - /// - /// Get object by identifier asynchronously. - /// - Task GetByIdAsync( - KeyType id, - CancellationToken cancellationToken = default(CancellationToken)); - - - /// - /// Get object using expression asynchronously. - /// - Task GetAsync( - Expression> where, - bool asNoTracking = false, - CancellationToken cancellationToken = default(CancellationToken)); - - - /// - /// Get all objects asynchronously. - /// - Task> GetAllAsync( - bool asNoTracking = false, - CancellationToken cancellationToken = default(CancellationToken)); - - - /// - /// Get objects using expression asynchronously. - /// - Task> GetManyAsync( - Expression> where, - bool asNoTracking = false, - CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/Source/BSN.Commons/Infrastructure/IUnitOfWork.cs b/Source/BSN.Commons/Infrastructure/IUnitOfWork.cs index 170bb51..9e2f3c1 100644 --- a/Source/BSN.Commons/Infrastructure/IUnitOfWork.cs +++ b/Source/BSN.Commons/Infrastructure/IUnitOfWork.cs @@ -5,7 +5,7 @@ namespace BSN.Commons.Infrastructure { - public interface IUnitOfWork : IDisposable, IAsyncDisposable + public interface IUnitOfWork : IDisposable { IDatabaseFactory DatabaseFactory { get; } @@ -14,8 +14,5 @@ public interface IUnitOfWork : IDisposable, IAsyncDisposable void AddToQueue(ITaskUnit task); void Commit(); - - Task CommitAsync( - CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/Source/BSN.Commons/Infrastructure/UnitOfWork.cs b/Source/BSN.Commons/Infrastructure/UnitOfWork.cs index fc96669..e49ebd0 100644 --- a/Source/BSN.Commons/Infrastructure/UnitOfWork.cs +++ b/Source/BSN.Commons/Infrastructure/UnitOfWork.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -7,205 +8,253 @@ namespace BSN.Commons.Infrastructure { - public class UnitOfWork : IUnitOfWork, IDisposable, IAsyncDisposable + /// + /// Represents a unit of work that coordinates database operations + /// and transaction-aware task units. + /// + public class UnitOfWork : Disposable, IUnitOfWork, IAsyncUnitOfWork { - private readonly Queue _tasks; + private readonly ConcurrentQueue _tasks; private readonly List _exceptions; + private readonly SemaphoreSlim _operationLock; + private readonly object _exceptionsLock; + private IDbContext _dataContext; - private bool _disposed; + private IAsyncDbContext _asyncDataContext; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The database factory that owns the database context. + /// + public UnitOfWork(IDatabaseFactory databaseFactory) + { + DatabaseFactory = databaseFactory + ?? throw new ArgumentNullException(nameof(databaseFactory)); - public IDatabaseFactory DatabaseFactory { get; private set; } + _tasks = new ConcurrentQueue(); + _exceptions = new List(); + + _operationLock = new SemaphoreSlim(1, 1); + _exceptionsLock = new object(); + } + /// + /// Gets the database factory. + /// + public IDatabaseFactory DatabaseFactory { get; } + + /// + /// Gets the exceptions raised by executed task units. + /// public IReadOnlyCollection Exceptions { - get { return _exceptions.AsReadOnly(); } + get + { + lock (_exceptionsLock) + { + return _exceptions.ToArray(); + } + } } + /// + /// Gets the database context. + /// protected IDbContext DataContext { get { - if (_dataContext == null) - { - _dataContext = DatabaseFactory.Get(); - } + ThrowIfDisposed(); - return _dataContext; + return _dataContext = _dataContext ?? DatabaseFactory.Get(); } } - - public UnitOfWork(IDatabaseFactory databaseFactory) + /// + /// Gets the database context for asynchronous operations. + /// + protected IAsyncDbContext AsyncDataContext { - if (databaseFactory == null) - throw new ArgumentNullException(nameof(databaseFactory)); - - DatabaseFactory = databaseFactory; + get + { + ThrowIfDisposed(); - _tasks = new Queue(); - _exceptions = new List(); + return _asyncDataContext = _asyncDataContext ?? DatabaseFactory.GetAsyncContext(); + } } - + /// public void AddToQueue(ITaskUnit task) { - if (task == null) - throw new ArgumentNullException(nameof(task)); + if (task == null) throw new ArgumentNullException(nameof(task)); - ThrowIfDisposed(); + _operationLock.Wait(); - _tasks.Enqueue(task); - } + try + { + ThrowIfDisposed(); + _tasks.Enqueue(task); + } + finally + { + _operationLock.Release(); + } + } + /// public void Commit() { - ThrowIfDisposed(); - - Queue executedTasks = - new Queue(); + _operationLock.Wait(); try { - using (var transaction = new TransactionScope()) + ThrowIfDisposed(); + + IEnumerable executedTasks = Enumerable.Empty(); + + try { - while (_tasks.Count > 0) + using (var transaction = new TransactionScope()) { - ITaskUnit task = _tasks.Dequeue(); + executedTasks = DequeueTasks(); - executedTasks.Enqueue(task); + DataContext.SaveChanges(); - Transaction.Current.EnlistVolatile( - task, - EnlistmentOptions.None); + transaction.Complete(); } - - DataContext.SaveChanges(); - - transaction.Complete(); } - } - catch - { - throw; + finally + { + CollectExceptions(executedTasks); + } } finally { - CollectExceptions(executedTasks); + _operationLock.Release(); } } - + /// public async Task CommitAsync( - CancellationToken cancellationToken = default(CancellationToken)) + CancellationToken cancellationToken = default) { - ThrowIfDisposed(); - - Queue executedTasks = - new Queue(); + await _operationLock.WaitAsync(cancellationToken); try { - using (var transaction = new TransactionScope( - TransactionScopeOption.Required, - new TransactionOptions - { - IsolationLevel = IsolationLevel.ReadCommitted - }, - TransactionScopeAsyncFlowOption.Enabled)) + ThrowIfDisposed(); + + IEnumerable executedTasks = Enumerable.Empty(); + try { - while (_tasks.Count > 0) + using (var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { - ITaskUnit task = _tasks.Dequeue(); + executedTasks = DequeueTasks(); - executedTasks.Enqueue(task); + await AsyncDataContext.SaveChangesAsync( + cancellationToken); - Transaction.Current.EnlistVolatile( - task, - EnlistmentOptions.None); + transaction.Complete(); } - - await DataContext.SaveChangesAsync(cancellationToken); - - transaction.Complete(); } - } - catch - { - throw; + finally + { + CollectExceptions(executedTasks); + } } finally { - CollectExceptions(executedTasks); + _operationLock.Release(); } } - private void CollectExceptions( - IEnumerable executedTasks) + /// + /// Dequeues all task units that belong to the current commit. + /// + private List DequeueTasks() { - _exceptions.Clear(); + var tasks = new List(); - foreach (ITaskUnit task in executedTasks) + while (_tasks.TryDequeue(out var task)) { - if (task.Exception != null) - { - _exceptions.Add(task.Exception); - } + Transaction.Current.EnlistVolatile( + task, + EnlistmentOptions.None); + tasks.Add(task); } - } + return tasks; + } - protected virtual void Dispose(bool disposing) + /// + /// Collects exceptions raised by executed task units. + /// + private void CollectExceptions( + IEnumerable executedTasks) { - if (_disposed) - return; - - if (disposing) + lock (_exceptionsLock) { - if (_dataContext != null) + _exceptions.Clear(); + + foreach (var task in executedTasks) { - _dataContext.Dispose(); - _dataContext = null; + if (task.Exception != null) + { + _exceptions.Add(task.Exception); + } } } - - _disposed = true; } - - public void Dispose() + /// + /// Throws an exception if the unit of work has been disposed. + /// + private void ThrowIfDisposed() { - Dispose(true); - - GC.SuppressFinalize(this); + if (IsDisposed) + { + throw new ObjectDisposedException( + GetType().FullName); + } } - - public async ValueTask DisposeAsync() + /// + /// Releases resources used by the unit of work. + /// + protected override void DisposeCore() { - if (_disposed) - return; + /* + * DatabaseFactory owns the database context. + * UnitOfWork must never dispose it. + * + * Waiting here guarantees that DisposeCore does not release + * UnitOfWork resources while AddToQueue/Commit is executing. + */ + _operationLock.Wait(); - if (_dataContext != null) + try { - await _dataContext.DisposeAsync(); _dataContext = null; - } - - _disposed = true; - - GC.SuppressFinalize(this); - } + _asyncDataContext = null; + while (_tasks.TryDequeue(out _)) + { + } - private void ThrowIfDisposed() - { - if (_disposed) + lock (_exceptionsLock) + { + _exceptions.Clear(); + } + } + finally { - throw new ObjectDisposedException( - GetType().FullName); + _operationLock.Release(); + _operationLock.Dispose(); } } } diff --git a/Test/BSN.Commons.Orm.EntityFramework.Tests/Data/UnitTestContext.cs b/Test/BSN.Commons.Orm.EntityFramework.Tests/Data/UnitTestContext.cs index d68093f..c4f6cc3 100644 --- a/Test/BSN.Commons.Orm.EntityFramework.Tests/Data/UnitTestContext.cs +++ b/Test/BSN.Commons.Orm.EntityFramework.Tests/Data/UnitTestContext.cs @@ -5,11 +5,11 @@ namespace BSN.Commons.Test.Data { - public class UnitTestContext : DbContext, IDbContext + public class UnitTestContext : DbContext, IDbContext, IAsyncDbContext { - public UnitTestContext(System.Data.Common.DbConnection dbConnection) : base(dbConnection, false) + public UnitTestContext(System.Data.Common.DbConnection dbConnection) : base(dbConnection, false) { - + } public UnitTestContext() @@ -33,15 +33,6 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder) base.OnModelCreating(modelBuilder); } - public override int SaveChanges() - { - return base.SaveChanges(); - } - public override Task SaveChangesAsync() - { - return base.SaveChangesAsync(); - } - public ValueTask DisposeAsync() { base.Dispose(); diff --git a/Test/BSN.Commons.Orm.EntityFramework.Tests/Infrastructure/DatabaseFactory.cs b/Test/BSN.Commons.Orm.EntityFramework.Tests/Infrastructure/DatabaseFactory.cs index 00a0bb3..2a4e6b2 100644 --- a/Test/BSN.Commons.Orm.EntityFramework.Tests/Infrastructure/DatabaseFactory.cs +++ b/Test/BSN.Commons.Orm.EntityFramework.Tests/Infrastructure/DatabaseFactory.cs @@ -46,5 +46,17 @@ IDbContext IDatabaseFactory.Get() else return (IDbContext)_dataContext; } + IAsyncDbContext IDatabaseFactory.GetAsyncContext() + { + if (_dataContext == null) + { + EffortConnection InMemoryconnection = DbConnectionFactory.CreateTransient(); + _dataContext = UnitTestContext.Create(InMemoryconnection); + _dataContext.Database.Initialize(false); + return (IAsyncDbContext)_dataContext; + } + else + return (IAsyncDbContext)_dataContext; + } } } diff --git a/Test/BSN.Commons.Orm.EntityFramework.Tests/Mock/UsersRepsitory.cs b/Test/BSN.Commons.Orm.EntityFramework.Tests/Mock/UsersRepsitory.cs index d596fec..52e16d4 100644 --- a/Test/BSN.Commons.Orm.EntityFramework.Tests/Mock/UsersRepsitory.cs +++ b/Test/BSN.Commons.Orm.EntityFramework.Tests/Mock/UsersRepsitory.cs @@ -4,7 +4,7 @@ namespace BSN.Commons.Test.Mock { - public sealed class UserRepository : RepositoryBase, IRepository + public sealed class UserRepository : RepositoryBase, IRepository, IAsyncRepository { public UserRepository(IDatabaseFactory databaseFactory) : base(databaseFactory) { } diff --git a/Test/BSN.Commons.Orm.EntityFramework.Tests/UnitOfWorkTest.cs b/Test/BSN.Commons.Orm.EntityFramework.Tests/UnitOfWorkTest.cs index 947b20a..8c6f54f 100644 --- a/Test/BSN.Commons.Orm.EntityFramework.Tests/UnitOfWorkTest.cs +++ b/Test/BSN.Commons.Orm.EntityFramework.Tests/UnitOfWorkTest.cs @@ -26,5 +26,10 @@ public override IRepository CreateUserRepository(IDatabaseFactory database { return new UserRepository(databaseFactory); } + + public override IAsyncRepository CreateAsyncUserRepository(IDatabaseFactory databaseFactory) + { + return new UserRepository(databaseFactory); + } } } diff --git a/Test/BSN.Commons.Orm.EntityFrameworkCore.Tests/Data/UnitTestContext.cs b/Test/BSN.Commons.Orm.EntityFrameworkCore.Tests/Data/UnitTestContext.cs index ce6df31..eaf4ff3 100644 --- a/Test/BSN.Commons.Orm.EntityFrameworkCore.Tests/Data/UnitTestContext.cs +++ b/Test/BSN.Commons.Orm.EntityFrameworkCore.Tests/Data/UnitTestContext.cs @@ -1,17 +1,12 @@ using BSN.Commons.Infrastructure; using BSN.Commons.Tests; using Microsoft.EntityFrameworkCore; -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Text; namespace BSN.Commons.Test.Data { - public class UnitTestContext : DbContext, IDbContext + public class UnitTestContext : DbContext, IDbContext, IAsyncDbContext { - public UnitTestContext(DbContextOptions options) : base(options) + public UnitTestContext(DbContextOptions options) : base(options) { } @@ -28,10 +23,5 @@ public static UnitTestContext Create(DbContextOptions options) public DbSet Users { get; set; } public DbSet Documents { get; set; } - - public override int SaveChanges() - { - return base.SaveChanges(); - } - } + } } diff --git a/Test/BSN.Commons.Orm.EntityFrameworkCore.Tests/Infrastructure/DatabaseFactory.cs b/Test/BSN.Commons.Orm.EntityFrameworkCore.Tests/Infrastructure/DatabaseFactory.cs index eb63299..603f0c1 100644 --- a/Test/BSN.Commons.Orm.EntityFrameworkCore.Tests/Infrastructure/DatabaseFactory.cs +++ b/Test/BSN.Commons.Orm.EntityFrameworkCore.Tests/Infrastructure/DatabaseFactory.cs @@ -1,9 +1,7 @@ -using BSN.Commons.Test.Data; -using BSN.Commons.Infrastructure; +using BSN.Commons.Infrastructure; +using BSN.Commons.Test.Data; using Microsoft.EntityFrameworkCore; using System; -using System.Collections.Generic; -using System.Text; namespace BSN.Commons.Test.Infrastructure { @@ -40,5 +38,19 @@ IDbContext IDatabaseFactory.Get() else return (IDbContext)_dataContext; } + + IAsyncDbContext IDatabaseFactory.GetAsyncContext() + { + if (_dataContext == null) + { + _dataContext = new UnitTestContext(new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options); + + return (IAsyncDbContext)_dataContext; + } + else + return (IAsyncDbContext)_dataContext; + } } } diff --git a/Test/BSN.Commons.Orm.EntityFrameworkCore.Tests/UnitOfWorkTest.cs b/Test/BSN.Commons.Orm.EntityFrameworkCore.Tests/UnitOfWorkTest.cs index 9e20e63..08b0d9a 100644 --- a/Test/BSN.Commons.Orm.EntityFrameworkCore.Tests/UnitOfWorkTest.cs +++ b/Test/BSN.Commons.Orm.EntityFrameworkCore.Tests/UnitOfWorkTest.cs @@ -26,5 +26,10 @@ public override IRepository CreateUserRepository(IDatabaseFactory database { return new UserRepository(databaseFactory); } + + public override IAsyncRepository CreateAsyncUserRepository(IDatabaseFactory databaseFactory) + { + return new UserRepository(databaseFactory); + } } } \ No newline at end of file diff --git a/Test/BSN.Commons.Orm.Redis.Tests/Data/UnitTestContext.cs b/Test/BSN.Commons.Orm.Redis.Tests/Data/UnitTestContext.cs index 50b45c7..93e2245 100644 --- a/Test/BSN.Commons.Orm.Redis.Tests/Data/UnitTestContext.cs +++ b/Test/BSN.Commons.Orm.Redis.Tests/Data/UnitTestContext.cs @@ -1,32 +1,18 @@ -using BSN.Commons.Infrastructure; -using BSN.Commons.Infrastructure.Redis; +using BSN.Commons.Infrastructure.Redis; using BSN.Commons.Orm.Redis; using BSN.Commons.Tests; using Microsoft.Extensions.Options; using Redis.OM.Searching; -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Text; namespace BSN.Commons.Test.Data { - public class UnitTestContext : Orm.Redis.DbContext, ICreatable, UnitTestContext> + public class UnitTestContext : DbContext, ICreatable, UnitTestContext> { - public UnitTestContext(IOptions options) : base(options) - { - - } + public UnitTestContext(IOptions options) : base(options) { } public new static UnitTestContext Create(IOptions options) => new UnitTestContext(options); public IRedisCollection Users { get; set; } public IRedisCollection Documents { get; set; } - - public override int SaveChanges() - { - return base.SaveChanges(); - } - } + } } diff --git a/Test/BSN.Commons.Tests/UnitOfWorkArrangementsAbstractFactory.cs b/Test/BSN.Commons.Tests/UnitOfWorkArrangementsAbstractFactory.cs index 998b944..d4ce975 100644 --- a/Test/BSN.Commons.Tests/UnitOfWorkArrangementsAbstractFactory.cs +++ b/Test/BSN.Commons.Tests/UnitOfWorkArrangementsAbstractFactory.cs @@ -6,6 +6,7 @@ public abstract class UnitOfWorkArrangementsAbstractFactory { public abstract IDatabaseFactory CreateDatabaseFactory(); public abstract IRepository CreateUserRepository(IDatabaseFactory databaseFactory); + public abstract IAsyncRepository CreateAsyncUserRepository(IDatabaseFactory databaseFactory); } } diff --git a/Test/BSN.Commons.Tests/UnitOfWorkTestBase.cs b/Test/BSN.Commons.Tests/UnitOfWorkTestBase.cs index 789ab15..dcce03e 100644 --- a/Test/BSN.Commons.Tests/UnitOfWorkTestBase.cs +++ b/Test/BSN.Commons.Tests/UnitOfWorkTestBase.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; +using System.Transactions; namespace BSN.Commons.Tests { @@ -18,6 +20,7 @@ public void SetUp() _databaseFactory = abstractFactory.CreateDatabaseFactory(); _userRepository = abstractFactory.CreateUserRepository(_databaseFactory); + _asyncuserRepository = abstractFactory.CreateAsyncUserRepository(_databaseFactory); } [TearDown] @@ -187,7 +190,7 @@ public void AddTaskAndDataForDatabase_ExceptionInjectedInTaskAndIncorectInput_Al new Task(() => { }) ); - unitOfWork.AddToQueue(enlistTask); + unitOfWork.AddToQueue(exceptionTask); try { @@ -231,7 +234,7 @@ public void AddTaskAndDataForDatabase_ExceptionInjectedInDataBase_AllTasksAndDat { unitOfWork.Commit(); } - catch + catch { Assert.That(Names.Where(P => P == "Gholi").FirstOrDefault(), Is.Null); Assert.That(_userRepository.GetMany(x => x.FirstName == "AliiReza"), Is.Empty); @@ -255,7 +258,7 @@ public void AddUserToDataBaseAndNullQueue_IncorrectInput_UsereShouldBeAddedToDat _userRepository.Add(User); unitOfWork.Commit(); } - catch + catch { Assert.That(_userRepository.GetMany(x => x.FirstName == "hamidReza"), Is.Empty); } @@ -272,14 +275,422 @@ public void NoTaskForQueueAndNoDataForDataBase_CorrectInput_ShouldHaveCorrectOut { unitOfWork.Commit(); } - catch + catch { Assert.Fail(); } Assert.That(Names, Is.Empty); } + [Test] + public async Task AddUserToDataBaseAndNullQueue_CorrectInput_UserShouldBeCorrectlyAddedToDatabaseAsync() + { + IAsyncUnitOfWork unitOfWork = + new UnitOfWork(_databaseFactory); + + var user = new User + { + FirstName = "AliReza", + LastName = "Alizadeh", + Password = "123456", + Document = new Document + { + Title = "AsyncTest" + } + }; + + await _asyncuserRepository.AddAsync(user); + + await unitOfWork.CommitAsync(); + + var result = await _asyncuserRepository.GetAsync( + x => x.FirstName == "AliReza"); + + Assert.That(result, Is.Not.Null); + Assert.That(result.FirstName, Is.EqualTo("AliReza")); + Assert.That(result.Document, Is.Not.Null); + Assert.That(result.Document.Title, Is.EqualTo("AsyncTest")); + } + + [Test] + public async Task AddTaskAndDataForDatabase_CorrectInput_AllTasksShouldBeExecutedAsync() + { + IAsyncUnitOfWork unitOfWork = + new UnitOfWork(_databaseFactory); + + var names = new List + { + "Reza", + "MohammadReza" + }; + + var enlistTask = new EnlistTask + ( + new Task(() => names.Add("Gholi")), + new Task(() => names.Remove("Gholi")) + ); + + var secondEnlistTask = new EnlistTask + ( + new Task(() => names.Add("Qamar")), + new Task(() => names.Remove("Qamar")) + ); + + unitOfWork.AddToQueue(enlistTask); + unitOfWork.AddToQueue(secondEnlistTask); + + await unitOfWork.CommitAsync(); + + Assert.That(names, Does.Contain("Gholi")); + Assert.That(names, Does.Contain("Qamar")); + } + + [Test] + public async Task AddTaskAndNullDatabase_ExceptionInjectedInput_AllTasksShouldBeRolledBackAsync() + { + IAsyncUnitOfWork unitOfWork = new UnitOfWork(_databaseFactory); + + List names = new List + { + "Reza", + "MohammadReza" + }; + + var enlistTask = new EnlistTask + ( + new Task(() => + { + names.Add("Qamar"); + }), + new Task(() => + { + names.Remove("Qamar"); + }) + ); + + unitOfWork.AddToQueue(enlistTask); + + var enlistTask2 = new EnlistTask + ( + new Task(() => + { + names.Add("Gholi"); + throw new Exception("Injected exception."); + }), + new Task(() => + { + names.Remove("Gholi"); + }) + ); + + unitOfWork.AddToQueue(enlistTask2); + + Assert.ThrowsAsync( + async () => await unitOfWork.CommitAsync()); + + // All task changes must be rolled back. + Assert.That(names.Contains("Qamar"), Is.False); + Assert.That(names.Contains("Gholi"), Is.False); + + // The original task exception must be collected. + Assert.That(unitOfWork.Exceptions, Has.Count.EqualTo(1)); + + Assert.That( + unitOfWork.Exceptions.Single().Message, + Is.EqualTo("Injected exception.")); + } + + [Test] + public async Task AddTaskAndDataForDatabase_ExceptionInjectedInTask_AllTasksAndDatabaseShouldBeRolledBackAsync() + { + IAsyncUnitOfWork unitOfWork = new UnitOfWork(_databaseFactory); + + User user = new User + { + FirstName = "Reza", + LastName = "Alizadeh", + Password = "123456", + Document = new Document + { + Title = "Test" + } + }; + + var addUser = new EnlistTask + ( + new Task(() => _userRepository.Add(user)), + new Task(() => _userRepository.Delete(user)) + ); + + unitOfWork.AddToQueue(addUser); + + List names = new List + { + "Reza", + "MohammadReza" + }; + + var addName = new EnlistTask + ( + new Task(() => + { + names.Add("Qamar"); + throw new Exception("Injected exception."); + }), + new Task(() => names.Remove("Qamar")) + ); + + unitOfWork.AddToQueue(addName); + + // Transaction must be aborted. + Assert.ThrowsAsync( + async () => await unitOfWork.CommitAsync()); + + // Database changes must be rolled back. + Assert.That( + _userRepository.GetMany(x => x.FirstName == "Reza"), + Is.Empty); + + // Task compensation must be executed. + Assert.That( + names.Contains("Qamar"), + Is.False); + + // UnitOfWork must collect the exception raised by the task. + Assert.That(unitOfWork.Exceptions, Is.Not.Empty); + + Assert.That( + unitOfWork.Exceptions.Any( + x => x.Message == "Injected exception."), + Is.True); + } + + [Test] + public async Task NoTaskForQueueAndNoDataForDatabase_CommitAsyncShouldCompleteSuccessfullyAsync() + { + IAsyncUnitOfWork unitOfWork = + new UnitOfWork(_databaseFactory); + + Assert.DoesNotThrowAsync( + async () => await unitOfWork.CommitAsync()); + } + + [Test] + public async Task CommitAsync_CancelledToken_ShouldThrowOperationCanceledExceptionAsync() + { + IAsyncUnitOfWork unitOfWork = + new UnitOfWork(_databaseFactory); + + using (var cancellationTokenSource = + new CancellationTokenSource()) + { + cancellationTokenSource.Cancel(); + + var exception = Assert.CatchAsync( + async () => + await unitOfWork.CommitAsync( + cancellationTokenSource.Token)); + + Assert.That(exception, Is.Not.Null); + } + } + + [Test] + public async Task CommitAsync_CancelledToken_UnitOfWorkShouldRemainUsableAsync() + { + IAsyncUnitOfWork unitOfWork = + new UnitOfWork(_databaseFactory); + + using (var cancellationTokenSource = + new CancellationTokenSource()) + { + cancellationTokenSource.Cancel(); + + Assert.CatchAsync( + async () => + await unitOfWork.CommitAsync( + cancellationTokenSource.Token)); + } + + Assert.DoesNotThrowAsync( + async () => await unitOfWork.CommitAsync()); + } + + [Test] + public void AddToQueue_AfterDispose_ShouldThrowObjectDisposedException() + { + IUnitOfWork unitOfWork = + new UnitOfWork(_databaseFactory); + + unitOfWork.Dispose(); + + var task = new EnlistTask( + new Task(() => { }), + new Task(() => { })); + + Assert.Throws( + () => unitOfWork.AddToQueue(task)); + } + + [Test] + public void Commit_AfterDispose_ShouldThrowObjectDisposedException() + { + IUnitOfWork unitOfWork = + new UnitOfWork(_databaseFactory); + + unitOfWork.Dispose(); + + Assert.Throws( + () => unitOfWork.Commit()); + } + + [Test] + public void CommitAsync_AfterDispose_ShouldThrowObjectDisposedException() + { + IUnitOfWork unitOfWork = + new UnitOfWork(_databaseFactory); + + unitOfWork.Dispose(); + + Assert.Throws( + () => unitOfWork.Commit()); + } + + [Test] + public void Dispose_MultipleTimes_ShouldNotThrow() + { + IUnitOfWork unitOfWork = + new UnitOfWork(_databaseFactory); + + Assert.DoesNotThrow(() => + { + unitOfWork.Dispose(); + unitOfWork.Dispose(); + unitOfWork.Dispose(); + }); + } + + [Test] + public async Task Exceptions_AfterFailedCommitAsync_ShouldContainTaskExceptionAsync() + { + IAsyncUnitOfWork unitOfWork = + new UnitOfWork(_databaseFactory); + + var exception = new Exception("Test exception."); + + unitOfWork.AddToQueue( + new EnlistTask( + new Task(() => throw exception), + new Task(() => { }))); + + Assert.CatchAsync( + async () => await unitOfWork.CommitAsync()); + + Assert.That(unitOfWork.Exceptions, Has.Count.EqualTo(1)); + + var collectedException = unitOfWork.Exceptions.Single(); + + Assert.That(collectedException, Is.SameAs(exception)); + Assert.That(collectedException.Message, Is.EqualTo("Test exception.")); + } + + [Test] + public async Task CommitAsync_ConcurrentCalls_ShouldNotExecuteConcurrentlyAsync() + { + IAsyncUnitOfWork unitOfWork = + new UnitOfWork(_databaseFactory); + + var commits = Enumerable + .Range(0, 10) + .Select(_ => unitOfWork.CommitAsync()) + .ToArray(); + + Assert.DoesNotThrowAsync( + async () => await Task.WhenAll(commits)); + } + + [Test] + public async Task AddToQueue_ConcurrentCalls_AllTasksShouldBeQueuedAsync() + { + IAsyncUnitOfWork unitOfWork = + new UnitOfWork(_databaseFactory); + + var names = new List(); + var tasks = new List(); + + for (int i = 0; i < 100; i++) + { + var name = $"Name-{i}"; + + tasks.Add( + new EnlistTask( + new Task(() => names.Add(name)), + new Task(() => names.Remove(name)))); + } + + await Task.WhenAll( + tasks.Select(task => + Task.Run(() => unitOfWork.AddToQueue(task)))); + + await unitOfWork.CommitAsync(); + + Assert.That(names.Count, Is.EqualTo(100)); + } + + [Test] + public async Task CommitAndCommitAsync_ConcurrentCalls_ShouldBeSerializedAsync() + { + IAsyncUnitOfWork asyncUnitOfWork = + new UnitOfWork(_databaseFactory); + + var names = new List(); + + var task = new EnlistTask + ( + new Task(() => names.Add("Test")), + new Task(() => names.Remove("Test")) + ); + + asyncUnitOfWork.AddToQueue(task); + + var syncUnitOfWork = + (IUnitOfWork)asyncUnitOfWork; + + var asyncCommit = asyncUnitOfWork.CommitAsync(); + + await Task.Run(() => syncUnitOfWork.Commit()); + + await asyncCommit; + + Assert.That(names, Does.Contain("Test")); + } + + [Test] + public async Task CommitAsync_AfterSuccessfulCommit_ShouldNotExecuteTaskAgainAsync() + { + IAsyncUnitOfWork unitOfWork = + new UnitOfWork(_databaseFactory); + + var executionCount = 0; + + var task = new EnlistTask + ( + new Task(() => Interlocked.Increment(ref executionCount)), + new Task(() => Interlocked.Decrement(ref executionCount)) + ); + + unitOfWork.AddToQueue(task); + + await unitOfWork.CommitAsync(); + + Assert.That(executionCount, Is.EqualTo(1)); + + await unitOfWork.CommitAsync(); + + Assert.That(executionCount, Is.EqualTo(1)); + } protected IRepository _userRepository; + protected IAsyncRepository _asyncuserRepository; protected IDatabaseFactory _databaseFactory; } From b5c1ba5d1ac45af6daa65511fe3a563f22c9c634 Mon Sep 17 00:00:00 2001 From: "m.rahimian" Date: Tue, 1 Sep 2026 13:05:18 +0330 Subject: [PATCH 10/11] add DatabaseFactory default implimentation for ef core --- .../DatabaseFactory.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 Source/BSN.Commons.Orm.EntityFrameworkCore/DatabaseFactory.cs diff --git a/Source/BSN.Commons.Orm.EntityFrameworkCore/DatabaseFactory.cs b/Source/BSN.Commons.Orm.EntityFrameworkCore/DatabaseFactory.cs new file mode 100644 index 0000000..9ae875b --- /dev/null +++ b/Source/BSN.Commons.Orm.EntityFrameworkCore/DatabaseFactory.cs @@ -0,0 +1,29 @@ +using BSN.Commons.Infrastructure; +using Microsoft.EntityFrameworkCore; + +namespace BSN.Commons.Orm.EntityFramework +{ + public class DatabaseFactory : Disposable, IDatabaseFactory where TContext : DbContext, IDbContext, IAsyncDbContext + { + public DatabaseFactory(TContext context) + { + _dataContext = context; + } + + public IDbContext Get() + { + return _dataContext; + } + + public void Dispose() + { + _dataContext?.Dispose(); + } + + public IAsyncDbContext GetAsyncContext() + { + return _dataContext; + } + protected TContext _dataContext; + } +} \ No newline at end of file From 82d27884a0dd0e937659d59ee5f40c3a8c96edb3 Mon Sep 17 00:00:00 2001 From: "m.rahimian" Date: Tue, 1 Sep 2026 13:06:31 +0330 Subject: [PATCH 11/11] change namespace --- Source/BSN.Commons.Orm.EntityFrameworkCore/DatabaseFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/BSN.Commons.Orm.EntityFrameworkCore/DatabaseFactory.cs b/Source/BSN.Commons.Orm.EntityFrameworkCore/DatabaseFactory.cs index 9ae875b..e16ac76 100644 --- a/Source/BSN.Commons.Orm.EntityFrameworkCore/DatabaseFactory.cs +++ b/Source/BSN.Commons.Orm.EntityFrameworkCore/DatabaseFactory.cs @@ -1,7 +1,7 @@ using BSN.Commons.Infrastructure; using Microsoft.EntityFrameworkCore; -namespace BSN.Commons.Orm.EntityFramework +namespace BSN.Commons.Orm.EntityFrameworkCore { public class DatabaseFactory : Disposable, IDatabaseFactory where TContext : DbContext, IDbContext, IAsyncDbContext {