diff --git a/Source/BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj b/Source/BSN.Commons.AutoMapper/BSN.Commons.AutoMapper.csproj index 4626a5c..8a5571f 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,8 @@ - - + + diff --git a/Source/BSN.Commons.AutoMapper/Extensions/IServiceCollectionExtensions.cs b/Source/BSN.Commons.AutoMapper/Extensions/IServiceCollectionExtensions.cs index edc2b4c..83eb153 100644 --- a/Source/BSN.Commons.AutoMapper/Extensions/IServiceCollectionExtensions.cs +++ b/Source/BSN.Commons.AutoMapper/Extensions/IServiceCollectionExtensions.cs @@ -20,7 +20,7 @@ public static IServiceCollection AddCommonsAutoMapper(this IServiceCollection se IMapper mapper = mappingConfig.CreateMapper(); services.AddSingleton(mapper); - + return services; } } 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 acad5be..8dd4b79 100644 --- a/Source/BSN.Commons.Orm.EntityFramework/RepositoryBase.cs +++ b/Source/BSN.Commons.Orm.EntityFramework/RepositoryBase.cs @@ -1,46 +1,85 @@ -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, IAsyncRepository + 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 +87,8 @@ public virtual void Update(T entity, Action> configurer) return; } - bool autoDetectChangesPreviousValue = _dataContext.Configuration.AutoDetectChangesEnabled; + bool previousValue = + _dataContext.Configuration.AutoDetectChangesEnabled; try { @@ -58,31 +98,45 @@ 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); + + configurer(updateConfig); if (updateConfig.AutoDetectChangedPropertiesEnabled) { @@ -90,92 +144,187 @@ public virtual void UpdateRange(IEnumerable entities, Action return; } - 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) - dbSet.Remove(obj); + var objects = dbSet.Where(where); + + DeleteRange(objects); } + /// - 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 Task GetByIdAsync( + KeyType id, + CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + + return dbSet.FindAsync( + cancellationToken, + id); + } + + + /// + public virtual IEnumerable GetAll( + bool asNoTracking = false) + { + 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); + } + + + /// + public virtual IEnumerable GetMany( + Expression> where, + bool asNoTracking = false) + { + IQueryable query = dbSet.Where(where); + + if (asNoTracking) + query = query.AsNoTracking(); + + return query; + } + + /// - public virtual IEnumerable GetAll() + public virtual async Task> GetManyAsync( + Expression> where, + bool asNoTracking = false, + CancellationToken cancellationToken = default(CancellationToken)) { - return dbSet.ToList(); + IQueryable query = dbSet.Where(where); + + if (asNoTracking) + query = query.AsNoTracking(); + + return await query + .ToListAsync(cancellationToken); } + /// - public virtual IEnumerable GetMany(Expression> where) + public virtual T Get( + Expression> where, + bool asNoTracking = false) { - return dbSet.Where(where); + IQueryable query = dbSet.Where(where); + + if (asNoTracking) + query = query.AsNoTracking(); + + return query.FirstOrDefault(); } + /// - public T Get(Expression> where) + public virtual async Task GetAsync( + Expression> where, + bool asNoTracking = false, + CancellationToken cancellationToken = default(CancellationToken)) { - return dbSet.Where(where).FirstOrDefault(); + 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()); /// - /// TODO: complete doc + /// Database Factory /// protected IDatabaseFactory DatabaseFactory { get; private set; } 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 44d1f8f..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 diff --git a/Source/BSN.Commons.Orm.EntityFrameworkCore/DatabaseFactory.cs b/Source/BSN.Commons.Orm.EntityFrameworkCore/DatabaseFactory.cs new file mode 100644 index 0000000..e16ac76 --- /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.EntityFrameworkCore +{ + 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.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 f3b2af8..69ce719 100644 --- a/Source/BSN.Commons.Orm.EntityFrameworkCore/RepositoryBase.cs +++ b/Source/BSN.Commons.Orm.EntityFrameworkCore/RepositoryBase.cs @@ -4,84 +4,190 @@ 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, IAsyncRepository + where T : class { - /// protected RepositoryBase(IDatabaseFactory databaseFactory) { + if (databaseFactory == null) + throw new ArgumentNullException(nameof(databaseFactory)); + DatabaseFactory = databaseFactory; dbSet = DataContext.Set(); } /// - public void Add(T entity) + public virtual void Add(T entity) { dbSet.Add(entity); } /// - public void AddRange(IEnumerable entities) + public virtual async Task AddAsync( + T entity, + CancellationToken cancellationToken = default) + { + await dbSet + .AddAsync(entity, cancellationToken) + .ConfigureAwait(false); + } + + /// + public virtual void AddRange(IEnumerable entities) { dbSet.AddRange(entities); } /// - public void Delete(T entity) + public virtual Task AddRangeAsync( + IEnumerable entities, + CancellationToken cancellationToken = default) + { + return dbSet.AddRangeAsync( + entities, + cancellationToken); + } + + /// + public virtual void Delete(T entity) { dbSet.Remove(entity); } /// - public void Delete(Expression> where) + public virtual void Delete(Expression> where) { - dbSet.RemoveRange(dbSet.Where(where)); + DeleteRange( + dbSet.Where(where)); } /// - public 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 async Task GetByIdAsync( + KeyType id, + CancellationToken cancellationToken = default) + { + return await dbSet + .FindAsync(id, + cancellationToken) + .ConfigureAwait(false); + } + + /// + public virtual IEnumerable GetAll( + bool asNoTracking = false) + { + IQueryable query = dbSet; + + if (asNoTracking) + query = query.AsNoTracking(); + + return query.ToList(); + } + + /// + public virtual async Task> GetAllAsync( + bool asNoTracking = false, + CancellationToken cancellationToken = default) + { + 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) { - return dbSet.ToList(); + IQueryable query = dbSet.Where(where); + + if (asNoTracking) + query = query.AsNoTracking(); + + return await query + .ToListAsync(cancellationToken); } /// - public virtual IEnumerable GetMany(Expression> where) + public virtual T Get( + Expression> where, + bool asNoTracking = false) { - return dbSet.Where(where); + IQueryable query = dbSet.Where(where); + + if (asNoTracking) + query = query.AsNoTracking(); + + return query.FirstOrDefault(); } /// - public T Get(Expression> where) + public virtual async Task GetAsync( + Expression> where, + bool asNoTracking = false, + CancellationToken cancellationToken = default) { - return dbSet.Where(where).FirstOrDefault(); + IQueryable query = dbSet.Where(where); + + if (asNoTracking) + query = query.AsNoTracking(); + + return await query + .FirstOrDefaultAsync(cancellationToken); } /// - public void Update(T entity) + public virtual void Update(T entity) { Update(entity, cfg => cfg.IncludeAllProperties()); } /// - public void Update(T entity, Action> configurer) + public virtual void Update( + T entity, + Action> configurer) { var updateConfig = new UpdateConfig(); - configurer.Invoke(updateConfig); + + configurer(updateConfig); // TODO: Why this behaviour exist? if (updateConfig.AutoDetectChangedPropertiesEnabled) @@ -90,7 +196,8 @@ public void Update(T entity, Action> configurer) return; } - bool autoDetectChangesPreviousValue = _dataContext.ChangeTracker.AutoDetectChangesEnabled; + bool previous = + _dataContext.ChangeTracker.AutoDetectChangesEnabled; try { @@ -100,31 +207,43 @@ 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) + public virtual void UpdateRange(IEnumerable entities) { - UpdateRange(entities, cfg => cfg.IncludeAllProperties()); + UpdateRange( + entities, + cfg => cfg.IncludeAllProperties()); } /// - public void UpdateRange(IEnumerable entities, Action> configurer) + public virtual void UpdateRange( + IEnumerable entities, + Action> configurer) { var updateConfig = new UpdateConfig(); - configurer.Invoke(updateConfig); + + configurer(updateConfig); if (updateConfig.AutoDetectChangedPropertiesEnabled) { @@ -132,51 +251,56 @@ public void UpdateRange(IEnumerable entities, Action> config return; } - 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 + /// Database Set /// protected readonly DbSet dbSet; /// - /// TODO: complete doc + /// Database Context /// protected DbContext DataContext => _dataContext ?? (_dataContext = (DbContext)DatabaseFactory.Get()); /// - /// 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.Redis/BSN.Commons.Orm.Redis.csproj b/Source/BSN.Commons.Orm.Redis/BSN.Commons.Orm.Redis.csproj index 57b3578..5ebcdad 100644 --- a/Source/BSN.Commons.Orm.Redis/BSN.Commons.Orm.Redis.csproj +++ b/Source/BSN.Commons.Orm.Redis/BSN.Commons.Orm.Redis.csproj @@ -73,7 +73,7 @@ - + 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 a16a325..686b1c5 100644 --- a/Source/BSN.Commons.Orm.Redis/DbContext.cs +++ b/Source/BSN.Commons.Orm.Redis/DbContext.cs @@ -2,16 +2,15 @@ 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 { /// /// Redis Database Context /// - public class DbContext : RedisConnectionProvider, IDbContext, ICreatable, DbContext> + public class DbContext : RedisConnectionProvider, IDbContext, IAsyncDbContext, ICreatable, DbContext> { /// /// Constructor of Redis Database Context @@ -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..94e77af 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, IAsyncRepository + where T : class { /// /// Constructor for Redis Repository Base @@ -25,108 +24,276 @@ 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) + { + if (entity == null) + throw new ArgumentNullException(nameof(entity)); + cancellationToken.ThrowIfCancellationRequested(); + + return dbCollection.InsertAsync(entity); + } + + + /// + public virtual void AddRange( + IEnumerable entities) { + if (entities == null) + throw new ArgumentNullException(nameof(entities)); + foreach (var entity in entities) { Add(entity); } } + + /// + public virtual Task AddRangeAsync( + IEnumerable entities, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return dbCollection.InsertAsync(entities); + } + + /// - public void Update(T entity) + 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 virtual T GetById( + KeyType id) + { + if (id == null) + throw new ArgumentNullException(nameof(id)); + + + if (id is string) + { + var entity = + dbCollection.FindById(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 T GetById(KeyType id) + public virtual async Task GetByIdAsync( + KeyType id, + CancellationToken cancellationToken = default) { - if (id is string str_id) + cancellationToken.ThrowIfCancellationRequested(); + if (id == null) + throw new ArgumentNullException(nameof(id)); + + if (id is string) { - T? entity = dbCollection.FindById(str_id); + var entity = await dbCollection.FindByIdAsync(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 virtual T Get( + Expression> where, + bool asNoTracking = false) + { + if (where == null) + throw new ArgumentNullException(nameof(where)); + + return dbCollection + .FirstOrDefault(where); } /// - public T Get(Expression> where) + public virtual Task GetAsync( + Expression> where, + bool asNoTracking = false, + CancellationToken cancellationToken = default) { - return dbCollection.Where(where).FirstOrDefault(); + cancellationToken.ThrowIfCancellationRequested(); + + if (where == null) + throw new ArgumentNullException(nameof(where)); + + return dbCollection + .FirstOrDefaultAsync(where); + } + + + /// + public virtual IEnumerable GetAll( + bool asNoTracking = false) + { + return dbCollection + .Where(entity => true) + .AsEnumerable(); } + /// - public IEnumerable GetAll() + public virtual async Task> GetAllAsync( + bool asNoTracking = false, + CancellationToken cancellationToken = default) { - return dbCollection.Where(entity => true); + cancellationToken.ThrowIfCancellationRequested(); + + var result = await dbCollection + .Where(entity => true) + .ToListAsync(); + + return result.AsEnumerable(); } + /// - 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 async Task> GetManyAsync( + Expression> where, + bool asNoTracking = false, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (where == null) + throw new ArgumentNullException(nameof(where)); + + var result = await dbCollection + .Where(where) + .ToListAsync(); + + return result.AsEnumerable(); + } + protected readonly IRedisCollection dbCollection; protected IDatabaseFactory DatabaseFactory { get; private set; } diff --git a/Source/BSN.Commons/BSN.Commons.csproj b/Source/BSN.Commons/BSN.Commons.csproj index 1ddd7ce..e9581df 100644 --- a/Source/BSN.Commons/BSN.Commons.csproj +++ b/Source/BSN.Commons/BSN.Commons.csproj @@ -53,8 +53,8 @@ - - + + 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 beeca36..26b0e3d 100644 --- a/Source/BSN.Commons/Infrastructure/IDbContext.cs +++ b/Source/BSN.Commons/Infrastructure/IDbContext.cs @@ -8,9 +8,8 @@ namespace BSN.Commons.Infrastructure public interface IDbContext : IDisposable { /// - /// Save changes to the database + /// Save changes to the database. /// - /// - int SaveChanges(); + 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 ac9ea2d..efa5b20 100644 --- a/Source/BSN.Commons/Infrastructure/IRepository.cs +++ b/Source/BSN.Commons/Infrastructure/IRepository.cs @@ -8,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. /// @@ -76,20 +76,23 @@ 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); + } } diff --git a/Source/BSN.Commons/Infrastructure/IUnitOfWork.cs b/Source/BSN.Commons/Infrastructure/IUnitOfWork.cs index e963c81..9e2f3c1 100644 --- a/Source/BSN.Commons/Infrastructure/IUnitOfWork.cs +++ b/Source/BSN.Commons/Infrastructure/IUnitOfWork.cs @@ -1,14 +1,18 @@ 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 { IDatabaseFactory DatabaseFactory { get; } - void Commit(); + IReadOnlyCollection Exceptions { get; } + void AddToQueue(ITaskUnit task); + + void Commit(); } -} +} \ No newline at end of file diff --git a/Source/BSN.Commons/Infrastructure/UnitOfWork.cs b/Source/BSN.Commons/Infrastructure/UnitOfWork.cs index bc2a46b..e49ebd0 100644 --- a/Source/BSN.Commons/Infrastructure/UnitOfWork.cs +++ b/Source/BSN.Commons/Infrastructure/UnitOfWork.cs @@ -1,62 +1,261 @@ using System; +using System.Collections.Concurrent; 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 + /// + /// Represents a unit of work that coordinates database operations + /// and transaction-aware task units. + /// + public class UnitOfWork : Disposable, IUnitOfWork, IAsyncUnitOfWork { + private readonly ConcurrentQueue _tasks; + private readonly List _exceptions; + + private readonly SemaphoreSlim _operationLock; + private readonly object _exceptionsLock; + + private IDbContext _dataContext; + 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)); + + _tasks = new ConcurrentQueue(); + _exceptions = new List(); + + _operationLock = new SemaphoreSlim(1, 1); + _exceptionsLock = new object(); + } + + /// + /// Gets the database factory. + /// public IDatabaseFactory DatabaseFactory { get; } - public List Exceptions { get; private set; } + /// + /// Gets the exceptions raised by executed task units. + /// + public IReadOnlyCollection Exceptions + { + get + { + lock (_exceptionsLock) + { + return _exceptions.ToArray(); + } + } + } - protected IDbContext DataContext => _dataContext ?? (_dataContext = DatabaseFactory.Get()); + /// + /// Gets the database context. + /// + protected IDbContext DataContext + { + get + { + ThrowIfDisposed(); + return _dataContext = _dataContext ?? DatabaseFactory.Get(); + } + } - public UnitOfWork(IDatabaseFactory databaseFactory) + /// + /// Gets the database context for asynchronous operations. + /// + protected IAsyncDbContext AsyncDataContext { - DatabaseFactory = databaseFactory; - _tasks = new Queue(); - Exceptions = new List(); + get + { + ThrowIfDisposed(); + + return _asyncDataContext = _asyncDataContext ?? DatabaseFactory.GetAsyncContext(); + } } + /// public void AddToQueue(ITaskUnit task) { - task = task ?? throw new ArgumentNullException(nameof(task)); - _tasks.Enqueue(task); + if (task == null) throw new ArgumentNullException(nameof(task)); + + _operationLock.Wait(); + + try + { + ThrowIfDisposed(); + + _tasks.Enqueue(task); + } + finally + { + _operationLock.Release(); + } } + /// public void Commit() { - 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()) { - var task = _tasks.Dequeue(); - executedTasks.Enqueue(task); - Transaction.Current.EnlistVolatile(task, EnlistmentOptions.None); - } + executedTasks = DequeueTasks(); + + DataContext.SaveChanges(); - DataContext.SaveChanges(); - transaction.Complete(); + transaction.Complete(); + } } + finally + { + CollectExceptions(executedTasks); + } + } + finally + { + _operationLock.Release(); } - catch (Exception ex) + } + + /// + public async Task CommitAsync( + CancellationToken cancellationToken = default) + { + await _operationLock.WaitAsync(cancellationToken); + + try { - throw ex; + ThrowIfDisposed(); + + IEnumerable executedTasks = Enumerable.Empty(); + try + { + using (var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + executedTasks = DequeueTasks(); + + await AsyncDataContext.SaveChangesAsync( + cancellationToken); + + transaction.Complete(); + } + } + finally + { + CollectExceptions(executedTasks); + } } finally { - Exceptions.AddRange(executedTasks.Select(a => a.Exception)); + _operationLock.Release(); } } - private IDbContext _dataContext; - private readonly Queue _tasks; + + /// + /// Dequeues all task units that belong to the current commit. + /// + private List DequeueTasks() + { + var tasks = new List(); + + while (_tasks.TryDequeue(out var task)) + { + Transaction.Current.EnlistVolatile( + task, + EnlistmentOptions.None); + tasks.Add(task); + } + + return tasks; + } + + /// + /// Collects exceptions raised by executed task units. + /// + private void CollectExceptions( + IEnumerable executedTasks) + { + lock (_exceptionsLock) + { + _exceptions.Clear(); + + foreach (var task in executedTasks) + { + if (task.Exception != null) + { + _exceptions.Add(task.Exception); + } + } + } + } + + /// + /// Throws an exception if the unit of work has been disposed. + /// + private void ThrowIfDisposed() + { + if (IsDisposed) + { + throw new ObjectDisposedException( + GetType().FullName); + } + } + + /// + /// Releases resources used by the unit of work. + /// + protected override void DisposeCore() + { + /* + * 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(); + + try + { + _dataContext = null; + _asyncDataContext = null; + + while (_tasks.TryDequeue(out _)) + { + } + + lock (_exceptionsLock) + { + _exceptions.Clear(); + } + } + finally + { + _operationLock.Release(); + _operationLock.Dispose(); + } + } } } \ No newline at end of file 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.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(); diff --git a/Test/BSN.Commons.Orm.EntityFramework.Tests/Data/UnitTestContext.cs b/Test/BSN.Commons.Orm.EntityFramework.Tests/Data/UnitTestContext.cs index 6f0e243..c4f6cc3 100644 --- a/Test/BSN.Commons.Orm.EntityFramework.Tests/Data/UnitTestContext.cs +++ b/Test/BSN.Commons.Orm.EntityFramework.Tests/Data/UnitTestContext.cs @@ -1,14 +1,15 @@ using BSN.Commons.Infrastructure; using BSN.Commons.Tests; using System.Data.Entity; +using System.Threading.Tasks; 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() @@ -32,9 +33,10 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder) base.OnModelCreating(modelBuilder); } - public override int SaveChanges() - { - return base.SaveChanges(); - } - } + public ValueTask DisposeAsync() + { + base.Dispose(); + return new ValueTask(); + } + } } 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 1f6ab48..52e16d4 100644 --- a/Test/BSN.Commons.Orm.EntityFramework.Tests/Mock/UsersRepsitory.cs +++ b/Test/BSN.Commons.Orm.EntityFramework.Tests/Mock/UsersRepsitory.cs @@ -1,11 +1,10 @@ -using BSN.Commons.Test.Data; -using BSN.Commons.Infrastructure; +using BSN.Commons.Infrastructure; using BSN.Commons.Orm.EntityFramework; using BSN.Commons.Tests; 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 f22ab06..8c6f54f 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 { @@ -31,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/BSN.Commons.Orm.Redis.Tests.csproj b/Test/BSN.Commons.Orm.Redis.Tests/BSN.Commons.Orm.Redis.Tests.csproj index 0adbbbb..9f87182 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/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.Orm.Redis.Tests/Infrastructure/DatabaseFactory.cs b/Test/BSN.Commons.Orm.Redis.Tests/Infrastructure/DatabaseFactory.cs index 30e3a78..7f057f9 100644 --- a/Test/BSN.Commons.Orm.Redis.Tests/Infrastructure/DatabaseFactory.cs +++ b/Test/BSN.Commons.Orm.Redis.Tests/Infrastructure/DatabaseFactory.cs @@ -1,21 +1,18 @@ -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; +using Testcontainers.Redis; 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()}" })) { 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 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; } 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; }