diff --git a/src/Volo.Abp.EntityFrameworkCore/Volo.Abp.EntityFrameworkCore.csproj b/src/Volo.Abp.EntityFrameworkCore/Volo.Abp.EntityFrameworkCore.csproj index 252c61d826..6f419752a4 100644 --- a/src/Volo.Abp.EntityFrameworkCore/Volo.Abp.EntityFrameworkCore.csproj +++ b/src/Volo.Abp.EntityFrameworkCore/Volo.Abp.EntityFrameworkCore.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DatabaseFacadeExtensions.cs b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DatabaseFacadeExtensions.cs new file mode 100644 index 0000000000..f663f43a0d --- /dev/null +++ b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DatabaseFacadeExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.DependencyInjection; + +namespace Volo.Abp.EntityFrameworkCore +{ + public static class DatabaseFacadeExtensions + { + public static bool IsRelational(this DatabaseFacade database) + { + return database.GetInfrastructure().GetService() != null; + } + } +} diff --git a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DbContextExtensions.cs b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DbContextExtensions.cs new file mode 100644 index 0000000000..3366186349 --- /dev/null +++ b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DbContextExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Volo.Abp.EntityFrameworkCore +{ + internal static class DbContextExtensions + { + public static bool HasRelationalTransactionManager(this DbContext dbContext) + { + return dbContext.Database.GetService() is IRelationalTransactionManager; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DependencyInjection/DbContextCreationContext.cs b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DependencyInjection/DbContextCreationContext.cs index b8a713c8aa..2b65b199a1 100644 --- a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DependencyInjection/DbContextCreationContext.cs +++ b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DependencyInjection/DbContextCreationContext.cs @@ -1,9 +1,10 @@ using System; +using System.Data.Common; using System.Threading; namespace Volo.Abp.EntityFrameworkCore.DependencyInjection { - internal class DbContextCreationContext + public class DbContextCreationContext { public static DbContextCreationContext Current => _current.Value; private static readonly AsyncLocal _current = new AsyncLocal(); @@ -12,6 +13,8 @@ namespace Volo.Abp.EntityFrameworkCore.DependencyInjection public string ConnectionString { get; } + public DbConnection ExistingConnection { get; set; } + public DbContextCreationContext(string connectionStringName, string connectionString) { ConnectionStringName = connectionStringName; diff --git a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DependencyInjection/DbContextOptionsFactory.cs b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DependencyInjection/DbContextOptionsFactory.cs index 047c10ba6c..c4c5ef40c4 100644 --- a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DependencyInjection/DbContextOptionsFactory.cs +++ b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DependencyInjection/DbContextOptionsFactory.cs @@ -12,39 +12,37 @@ namespace Volo.Abp.EntityFrameworkCore.DependencyInjection public static DbContextOptions Create(IServiceProvider serviceProvider) where TDbContext : AbpDbContext { - using (var scope = serviceProvider.CreateScope()) - { - var creationContext = GetCreationContext(scope.ServiceProvider); - - var context = new AbpDbContextConfigurationContext( - creationContext.ConnectionString, - creationContext.ConnectionStringName, - scope.ServiceProvider - ); + var creationContext = GetCreationContext(serviceProvider); - var dbContextOptions = GetDbContextOptions(scope); + var context = new AbpDbContextConfigurationContext( + creationContext.ConnectionString, + creationContext.ConnectionStringName, + serviceProvider + ); - var configureAction = dbContextOptions.ConfigureActions.GetOrDefault(typeof(TDbContext)); - if (configureAction != null) - { - ((Action>)configureAction).Invoke(context); - } - else if (dbContextOptions.DefaultConfigureAction != null) - { - dbContextOptions.DefaultConfigureAction.Invoke(context); - } - else - { - throw new AbpException($"No configuration found for {typeof(DbContext).AssemblyQualifiedName}! Use services.Configure(...) to configure it."); - } + var dbContextOptions = GetDbContextOptions(serviceProvider); - return context.DbContextOptions.Options; + var configureAction = dbContextOptions.ConfigureActions.GetOrDefault(typeof(TDbContext)); + if (configureAction != null) + { + ((Action>)configureAction).Invoke(context); + } + else if (dbContextOptions.DefaultConfigureAction != null) + { + dbContextOptions.DefaultConfigureAction.Invoke(context); + } + else + { + throw new AbpException($"No configuration found for {typeof(DbContext).AssemblyQualifiedName}! Use services.Configure(...) to configure it."); } + + return context.DbContextOptions.Options; } - private static AbpDbContextOptions GetDbContextOptions(IServiceScope scope) where TDbContext : AbpDbContext + private static AbpDbContextOptions GetDbContextOptions(IServiceProvider serviceProvider) + where TDbContext : AbpDbContext { - return scope.ServiceProvider.GetRequiredService>().Value; + return serviceProvider.GetRequiredService>().Value; } private static DbContextCreationContext GetCreationContext(IServiceProvider serviceProvider) diff --git a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/DbContextEfCoreTransactionStrategy.cs b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/DbContextEfCoreTransactionStrategy.cs new file mode 100644 index 0000000000..b1da0f8a71 --- /dev/null +++ b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/DbContextEfCoreTransactionStrategy.cs @@ -0,0 +1,46 @@ +using System.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore.DependencyInjection; + +namespace Volo.Abp.Uow.EntityFrameworkCore +{ + public class DbContextEfCoreTransactionStrategy : IEfCoreTransactionStrategy, ITransientDependency + { + public TDbContext CreateDbContext(IUnitOfWork unitOfWork, DbContextCreationContext creationContext) where TDbContext : DbContext + { + var transactionApiKey = $"EntityFrameworkCore_{creationContext.ConnectionString}"; + var activeTransaction = unitOfWork.FindTransactionApi(transactionApiKey) as IEfCoreTransactionApi; + + TDbContext dbContext; + if (activeTransaction == null) + { + dbContext = unitOfWork.ServiceProvider.GetRequiredService(); + var dbtransaction = dbContext.Database.BeginTransaction((unitOfWork.Options.IsolationLevel ?? IsolationLevel.ReadUncommitted)); + activeTransaction = new EfCoreTransactionApi(dbtransaction, dbContext); + unitOfWork.AddTransactionApi(transactionApiKey, activeTransaction); + } + else + { + creationContext.ExistingConnection = activeTransaction.DbContextTransaction.GetDbTransaction().Connection; + dbContext = unitOfWork.ServiceProvider.GetRequiredService(); + + if (dbContext.HasRelationalTransactionManager()) + { + dbContext.Database.UseTransaction(activeTransaction.DbContextTransaction.GetDbTransaction()); + } + else + { + dbContext.Database.BeginTransaction(); + } + + activeTransaction.AttendedDbContexts.Add(dbContext); + } + + return dbContext; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/EfCoreTransactionApi.cs b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/EfCoreTransactionApi.cs new file mode 100644 index 0000000000..4bedb5e0ea --- /dev/null +++ b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/EfCoreTransactionApi.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using Volo.Abp.EntityFrameworkCore; + +namespace Volo.Abp.Uow.EntityFrameworkCore +{ + public class EfCoreTransactionApi : IEfCoreTransactionApi + { + public IDbContextTransaction DbContextTransaction { get; } + public DbContext StarterDbContext { get; } + public List AttendedDbContexts { get; } + + public EfCoreTransactionApi(IDbContextTransaction dbContextTransaction, DbContext starterDbContext) + { + DbContextTransaction = dbContextTransaction; + StarterDbContext = starterDbContext; + AttendedDbContexts = new List(); + } + + public void Commit() + { + DbContextTransaction.Commit(); + + foreach (var dbContext in AttendedDbContexts) + { + if (dbContext.HasRelationalTransactionManager()) + { + continue; //Relational databases use the shared transaction + } + + dbContext.Database.CommitTransaction(); + } + } + + public Task CommitAsync() + { + Commit(); + return Task.CompletedTask; + } + + public void Dispose() + { + DbContextTransaction.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/IEfCoreTransactionApi.cs b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/IEfCoreTransactionApi.cs new file mode 100644 index 0000000000..881b8d202b --- /dev/null +++ b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/IEfCoreTransactionApi.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Volo.Abp.Uow.EntityFrameworkCore +{ + public interface IEfCoreTransactionApi : ITransactionApi + { + IDbContextTransaction DbContextTransaction { get; } + + DbContext StarterDbContext { get; } + + List AttendedDbContexts { get; } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/IEfCoreTransactionStrategy.cs b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/IEfCoreTransactionStrategy.cs new file mode 100644 index 0000000000..9536e4cad8 --- /dev/null +++ b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/IEfCoreTransactionStrategy.cs @@ -0,0 +1,11 @@ +using Microsoft.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore.DependencyInjection; + +namespace Volo.Abp.Uow.EntityFrameworkCore +{ + public interface IEfCoreTransactionStrategy + { + TDbContext CreateDbContext(IUnitOfWork unitOfWork, DbContextCreationContext creationContext) + where TDbContext : DbContext; + } +} diff --git a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/UnitOfWorkDbContextProvider.cs b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/UnitOfWorkDbContextProvider.cs index 717a68ebe7..b4f573b26d 100644 --- a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/UnitOfWorkDbContextProvider.cs +++ b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/UnitOfWorkDbContextProvider.cs @@ -1,3 +1,5 @@ +using System; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Data; using Volo.Abp.EntityFrameworkCore; @@ -5,18 +7,23 @@ using Volo.Abp.EntityFrameworkCore.DependencyInjection; namespace Volo.Abp.Uow.EntityFrameworkCore { + //TODO: Implement logic in DefaultDbContextResolver.Resolve in old ABP. + public class UnitOfWorkDbContextProvider : IDbContextProvider where TDbContext : AbpDbContext { private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IConnectionStringResolver _connectionStringResolver; + private readonly IEfCoreTransactionStrategy _transactionStrategy; public UnitOfWorkDbContextProvider( IUnitOfWorkManager unitOfWorkManager, - IConnectionStringResolver connectionStringResolver) + IConnectionStringResolver connectionStringResolver, + IEfCoreTransactionStrategy transactionStrategy) { _unitOfWorkManager = unitOfWorkManager; _connectionStringResolver = connectionStringResolver; + _transactionStrategy = transactionStrategy; } public TDbContext GetDbContext() @@ -41,12 +48,30 @@ namespace Volo.Abp.Uow.EntityFrameworkCore return ((DbContextDatabaseApi)databaseApi).DbContext; } - private static TDbContext CreateDbContext(IUnitOfWork unitOfWork, string connectionStringName, string connectionString) + private TDbContext CreateDbContext(IUnitOfWork unitOfWork, string connectionStringName, string connectionString) { var creationContext = new DbContextCreationContext(connectionStringName, connectionString); using (DbContextCreationContext.Use(creationContext)) { - return unitOfWork.ServiceProvider.GetRequiredService(); + TDbContext dbContext; + + if (unitOfWork.Options.IsTransactional == true) + { + dbContext = _transactionStrategy.CreateDbContext(unitOfWork, creationContext); + } + else + { + dbContext = unitOfWork.ServiceProvider.GetRequiredService(); + } + + if (unitOfWork.Options.Timeout.HasValue && + dbContext.Database.IsRelational() && + !dbContext.Database.GetCommandTimeout().HasValue) + { + dbContext.Database.SetCommandTimeout(unitOfWork.Options.Timeout.Value.TotalSeconds.To()); + } + + return dbContext; } } } diff --git a/src/Volo.Abp/Volo/Abp/Uow/ChildUnitOfWork.cs b/src/Volo.Abp/Volo/Abp/Uow/ChildUnitOfWork.cs index 15e03332f6..8e848aae34 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/ChildUnitOfWork.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/ChildUnitOfWork.cs @@ -83,7 +83,17 @@ namespace Volo.Abp.Uow { return _parent.GetOrAddDatabaseApi(key, factory); } - + + public ITransactionApi FindTransactionApi(string key) + { + return _parent.FindTransactionApi(key); + } + + public void AddTransactionApi(string key, ITransactionApi api) + { + _parent.AddTransactionApi(key, api); + } + public void Dispose() { diff --git a/src/Volo.Abp/Volo/Abp/Uow/ITransactionApi.cs b/src/Volo.Abp/Volo/Abp/Uow/ITransactionApi.cs new file mode 100644 index 0000000000..1f6f9aa226 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Uow/ITransactionApi.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.Uow +{ + public interface ITransactionApi + { + void Commit(); + + Task CommitAsync(); + + void Dispose(); + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/ITransactionApiContainer.cs b/src/Volo.Abp/Volo/Abp/Uow/ITransactionApiContainer.cs new file mode 100644 index 0000000000..dde7ace71f --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Uow/ITransactionApiContainer.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; + +namespace Volo.Abp.Uow +{ + public interface ITransactionApiContainer + { + [CanBeNull] + ITransactionApi FindTransactionApi([NotNull] string key); + + void AddTransactionApi([NotNull] string key, [NotNull] ITransactionApi api); + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWork.cs b/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWork.cs index 442c03c075..49352a3bd2 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWork.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWork.cs @@ -2,7 +2,7 @@ namespace Volo.Abp.Uow { - public interface IUnitOfWork : IBasicUnitOfWork, IDatabaseApiContainer + public interface IUnitOfWork : IBasicUnitOfWork, IDatabaseApiContainer, ITransactionApiContainer { UnitOfWorkStartOptions Options { get; } diff --git a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWork.cs b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWork.cs index 2409fbd4ba..e0e6beaf02 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWork.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWork.cs @@ -35,6 +35,7 @@ namespace Volo.Abp.Uow public IServiceProvider ServiceProvider { get; } private readonly Dictionary _databaseApis; + private readonly Dictionary _transactionApis; private Exception _exception; private bool _isCompleted; @@ -45,6 +46,7 @@ namespace Volo.Abp.Uow ServiceProvider = serviceProvider; _databaseApis = new Dictionary(); + _transactionApis = new Dictionary(); } public void SaveChanges() @@ -72,6 +74,7 @@ namespace Volo.Abp.Uow try { SaveChanges(); + CommitTransactions(); OnCompleted(); } catch (Exception ex) @@ -88,6 +91,7 @@ namespace Volo.Abp.Uow try { await SaveChangesAsync(cancellationToken); + await CommitTransactionsAsync(); OnCompleted(); } catch (Exception ex) @@ -110,6 +114,26 @@ namespace Volo.Abp.Uow return _databaseApis.GetOrAdd(key, factory); } + public ITransactionApi FindTransactionApi(string key) + { + Check.NotNull(key, nameof(key)); + + return _transactionApis.GetOrDefault(key); + } + + public void AddTransactionApi(string key, ITransactionApi api) + { + Check.NotNull(key, nameof(key)); + Check.NotNull(api, nameof(api)); + + if (_transactionApis.ContainsKey(key)) + { + throw new AbpException("There is already a transaction API in this unit of work!"); + } + + _transactionApis.Add(key, api); + } + protected virtual void OnCompleted() { Completed.InvokeSafely(this); @@ -156,5 +180,21 @@ namespace Volo.Abp.Uow { return $"[UnitOfWork {Id}]"; } + + protected virtual void CommitTransactions() + { + foreach (var transaction in _transactionApis.Values) + { + transaction.Commit(); + } + } + + protected virtual async Task CommitTransactionsAsync() + { + foreach (var transaction in _transactionApis.Values) + { + await transaction.CommitAsync(); + } + } } } \ No newline at end of file