From dcd643ea789808c94822a65b4c67f94e70e305be Mon Sep 17 00:00:00 2001 From: "m.rahimian" Date: Mon, 3 Aug 2026 15:14:26 +0330 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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