diff --git a/Volo.Abp.sln b/Volo.Abp.sln index 0f4cb065d2..a7eb296773 100644 --- a/Volo.Abp.sln +++ b/Volo.Abp.sln @@ -138,6 +138,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Identity.Domain", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Identity.Domain.Shared", "src\Volo.Abp.Identity.Domain.Shared\Volo.Abp.Identity.Domain.Shared.csproj", "{DF676F73-3FC9-46CE-909A-2D75E19982AD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.EntityFrameworkCore.Tests", "test\Volo.Abp.EntityFrameworkCore.Tests\Volo.Abp.EntityFrameworkCore.Tests.csproj", "{3AF7C7F5-6513-47D4-8DD0-6E1AF14568D8}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleConsoleDemo", "test\SimpleConsoleDemo\SimpleConsoleDemo.csproj", "{2B48CF90-DBDB-469F-941C-5B5AECEEACE0}" EndProject Global @@ -350,6 +351,10 @@ Global {DF676F73-3FC9-46CE-909A-2D75E19982AD}.Debug|Any CPU.Build.0 = Debug|Any CPU {DF676F73-3FC9-46CE-909A-2D75E19982AD}.Release|Any CPU.ActiveCfg = Release|Any CPU {DF676F73-3FC9-46CE-909A-2D75E19982AD}.Release|Any CPU.Build.0 = Release|Any CPU + {3AF7C7F5-6513-47D4-8DD0-6E1AF14568D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AF7C7F5-6513-47D4-8DD0-6E1AF14568D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AF7C7F5-6513-47D4-8DD0-6E1AF14568D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AF7C7F5-6513-47D4-8DD0-6E1AF14568D8}.Release|Any CPU.Build.0 = Release|Any CPU {2B48CF90-DBDB-469F-941C-5B5AECEEACE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2B48CF90-DBDB-469F-941C-5B5AECEEACE0}.Debug|Any CPU.Build.0 = Debug|Any CPU {2B48CF90-DBDB-469F-941C-5B5AECEEACE0}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -419,6 +424,7 @@ Global {40E21A35-1C66-4E89-A16E-0475011F7EFD} = {146F561E-C7B8-4166-9383-47E1BC1A2E62} {43D4005C-4F04-4128-937B-52BEAC5A113B} = {1895A5C9-50D4-4568-9A3A-14657E615A5E} {DF676F73-3FC9-46CE-909A-2D75E19982AD} = {1895A5C9-50D4-4568-9A3A-14657E615A5E} + {3AF7C7F5-6513-47D4-8DD0-6E1AF14568D8} = {37087D1B-3693-4E96-983D-A69F210BDE53} {2B48CF90-DBDB-469F-941C-5B5AECEEACE0} = {37087D1B-3693-4E96-983D-A69F210BDE53} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/AbpDesk/Web_PlugIns/AbpDesk.MongoBlog.dll b/src/AbpDesk/Web_PlugIns/AbpDesk.MongoBlog.dll index c5f958350e..8d7194d4e6 100644 Binary files a/src/AbpDesk/Web_PlugIns/AbpDesk.MongoBlog.dll and b/src/AbpDesk/Web_PlugIns/AbpDesk.MongoBlog.dll differ diff --git a/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Builder/AbpAspNetCoreMvcApplicationBuilderExtensions.cs b/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Builder/AbpAspNetCoreMvcApplicationBuilderExtensions.cs new file mode 100644 index 0000000000..34c669da52 --- /dev/null +++ b/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Builder/AbpAspNetCoreMvcApplicationBuilderExtensions.cs @@ -0,0 +1,12 @@ +using Volo.Abp.AspNetCore.Mvc.Uow; + +namespace Microsoft.AspNetCore.Builder +{ + public static class AbpAspNetCoreMvcApplicationBuilderExtensions + { + public static IApplicationBuilder UseUnitOfWork(this IApplicationBuilder app) + { + return app.UseMiddleware(); + } + } +} diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpController.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpController.cs index d3d49950cf..47901a01c3 100644 --- a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpController.cs +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpController.cs @@ -1,10 +1,27 @@ -using Microsoft.AspNetCore.Mvc; +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp.DependencyInjection; +using Volo.Abp.Guids; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Uow; namespace Volo.Abp.AspNetCore.Mvc { public abstract class AbpController : Controller, ITransientDependency { + public IUnitOfWorkManager UnitOfWorkManager { get; set; } + public IObjectMapper ObjectMapper { get; set; } + + public IGuidGenerator GuidGenerator { get; set; } + + public ILoggerFactory LoggerFactory { get; set; } + + protected IUnitOfWork CurrentUnitOfWork => UnitOfWorkManager?.Current; + + protected ILogger Logger => _lazyLogger.Value; + private Lazy _lazyLogger => new Lazy(() => LoggerFactory?.CreateLogger(GetType().FullName) ?? NullLogger.Instance, true); } } diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs index 53ae0da753..93517776b6 100644 --- a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Mvc.ExceptionHandling; +using Volo.Abp.AspNetCore.Mvc.Uow; using Volo.Abp.AspNetCore.Mvc.Validation; namespace Volo.Abp.AspNetCore.Mvc @@ -24,7 +25,7 @@ namespace Volo.Abp.AspNetCore.Mvc //options.Filters.AddService(typeof(AbpAuthorizationFilter)); //options.Filters.AddService(typeof(AbpAuditActionFilter)); options.Filters.AddService(typeof(AbpValidationActionFilter)); - //options.Filters.AddService(typeof(AbpUowActionFilter)); + options.Filters.AddService(typeof(AbpUowActionFilter)); options.Filters.AddService(typeof(AbpExceptionFilter)); //options.Filters.AddService(typeof(AbpResultFilter)); } diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUnitOfWorkMiddleware.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUnitOfWorkMiddleware.cs new file mode 100644 index 0000000000..dffa833f2b --- /dev/null +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUnitOfWorkMiddleware.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Volo.Abp.Uow; + +namespace Volo.Abp.AspNetCore.Mvc.Uow +{ + public class AbpUnitOfWorkMiddleware + { + private readonly RequestDelegate _next; + + public AbpUnitOfWorkMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext httpContext, IUnitOfWorkManager unitOfWorkManager) + { + using (var uow = unitOfWorkManager.Reserve(AbpUowActionFilter.UnitOfWorkReservationName)) + { + await _next(httpContext); + await uow.CompleteAsync(httpContext.RequestAborted); + } + } + } +} diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowActionFilter.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowActionFilter.cs new file mode 100644 index 0000000000..7a98a7729e --- /dev/null +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowActionFilter.cs @@ -0,0 +1,72 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Uow; + +namespace Volo.Abp.AspNetCore.Mvc.Uow +{ + public class AbpUowActionFilter : IAsyncActionFilter, ITransientDependency + { + public const string UnitOfWorkReservationName = "_AbpActionUnitOfWork"; + + private readonly IUnitOfWorkManager _unitOfWorkManager; + private readonly UnitOfWorkDefaultOptions _defaultOptions; + + public AbpUowActionFilter(IUnitOfWorkManager unitOfWorkManager, IOptions options) + { + _unitOfWorkManager = unitOfWorkManager; + _defaultOptions = options.Value; + } + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + if (!context.ActionDescriptor.IsControllerAction()) + { + await next(); + return; + } + + var methodInfo = context.ActionDescriptor.GetMethodInfo(); + var unitOfWorkAttr = UnitOfWorkHelper.GetUnitOfWorkAttributeOrNull(methodInfo); + + if (unitOfWorkAttr?.IsDisabled == true) + { + await next(); + return; + } + + var options = CreateOptions(context, unitOfWorkAttr); + + if (_unitOfWorkManager.TryBeginReserved(UnitOfWorkReservationName, options)) + { + await next(); + return; + } + + using (var uow = _unitOfWorkManager.Begin(options)) + { + var result = await next(); + if (result.Exception == null || result.ExceptionHandled) + { + await uow.CompleteAsync(context.HttpContext.RequestAborted); + } + } + } + + private UnitOfWorkOptions CreateOptions(ActionExecutingContext context, UnitOfWorkAttribute unitOfWorkAttr) + { + var options = new UnitOfWorkOptions(); + + unitOfWorkAttr?.SetOptions(options); + + options.IsTransactional = _defaultOptions.CalculateIsTransactional( + autoValue: !string.Equals(context.HttpContext.Request.Method, HttpMethod.Get.Method, StringComparison.OrdinalIgnoreCase) + ); + + return options; + } + } +} 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 new file mode 100644 index 0000000000..2b65b199a1 --- /dev/null +++ b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DependencyInjection/DbContextCreationContext.cs @@ -0,0 +1,31 @@ +using System; +using System.Data.Common; +using System.Threading; + +namespace Volo.Abp.EntityFrameworkCore.DependencyInjection +{ + public class DbContextCreationContext + { + public static DbContextCreationContext Current => _current.Value; + private static readonly AsyncLocal _current = new AsyncLocal(); + + public string ConnectionStringName { get; } + + public string ConnectionString { get; } + + public DbConnection ExistingConnection { get; set; } + + public DbContextCreationContext(string connectionStringName, string connectionString) + { + ConnectionStringName = connectionStringName; + ConnectionString = connectionString; + } + + public static IDisposable Use(DbContextCreationContext context) + { + var previousValue = Current; + _current.Value = context; + return new DisposeAction(() => _current.Value = previousValue); + } + } +} \ No newline at end of file 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 29a9572977..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,44 +12,55 @@ namespace Volo.Abp.EntityFrameworkCore.DependencyInjection public static DbContextOptions Create(IServiceProvider serviceProvider) where TDbContext : AbpDbContext { - var connectionStringName = ConnectionStringNameAttribute.GetConnStringName(); + var creationContext = GetCreationContext(serviceProvider); + + var context = new AbpDbContextConfigurationContext( + creationContext.ConnectionString, + creationContext.ConnectionStringName, + serviceProvider + ); - using (var scope = serviceProvider.CreateScope()) + var dbContextOptions = GetDbContextOptions(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 { - var context = new AbpDbContextConfigurationContext( - GetConnectionString(scope, connectionStringName), - connectionStringName, - scope.ServiceProvider - ); - - var dbContextOptions = GetDbContextOptions(scope); - - 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; + 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 string GetConnectionString(IServiceScope scope, string connectionStringName) + private static DbContextCreationContext GetCreationContext(IServiceProvider serviceProvider) + where TDbContext : AbpDbContext { - return scope.ServiceProvider.GetRequiredService().Resolve(connectionStringName); + var context = DbContextCreationContext.Current; + if (context != null) + { + return context; + } + + var connectionStringName = ConnectionStringNameAttribute.GetConnStringName(); + var connectionString = serviceProvider.GetRequiredService().Resolve(connectionStringName); + + return new DbContextCreationContext( + connectionStringName, + connectionString + ); } } } \ No newline at end of file diff --git a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/DbContextDatabaseApi.cs b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/EfCoreDatabaseApi.cs similarity index 79% rename from src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/DbContextDatabaseApi.cs rename to src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/EfCoreDatabaseApi.cs index 41fb5b8610..b73b237000 100644 --- a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/DbContextDatabaseApi.cs +++ b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/EfCoreDatabaseApi.cs @@ -4,12 +4,12 @@ using Volo.Abp.EntityFrameworkCore; namespace Volo.Abp.Uow.EntityFrameworkCore { - public class DbContextDatabaseApi : IDatabaseApi, ISupportsSavingChanges + public class EfCoreDatabaseApi : IDatabaseApi, ISupportsSavingChanges where TDbContext : AbpDbContext { public TDbContext DbContext { get; } - public DbContextDatabaseApi(TDbContext dbContext) + public EfCoreDatabaseApi(TDbContext dbContext) { DbContext = dbContext; } 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..a715d8c150 --- /dev/null +++ b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/EfCoreTransactionApi.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using Volo.Abp.EntityFrameworkCore; + +namespace Volo.Abp.Uow.EntityFrameworkCore +{ + public class EfCoreTransactionApi : ITransactionApi, ISupportsRollback + { + 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(); + } + + public void Rollback() + { + DbContextTransaction.Rollback(); + } + + public Task RollbackAsync(CancellationToken cancellationToken) + { + DbContextTransaction.Rollback(); + return Task.CompletedTask; + } + } +} \ No newline at end of file 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 2aae39d871..596fc72917 100644 --- a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/UnitOfWorkDbContextProvider.cs +++ b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/UnitOfWorkDbContextProvider.cs @@ -1,9 +1,15 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Data; using Volo.Abp.EntityFrameworkCore; +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 { @@ -26,16 +32,87 @@ namespace Volo.Abp.Uow.EntityFrameworkCore throw new AbpException("A DbContext can only be created inside a unit of work!"); } - var connectionString = _connectionStringResolver.Resolve(); + var connectionStringName = ConnectionStringNameAttribute.GetConnStringName(); + var connectionString = _connectionStringResolver.Resolve(connectionStringName); + var dbContextKey = $"{typeof(TDbContext).FullName}_{connectionString}"; var databaseApi = unitOfWork.GetOrAddDatabaseApi( dbContextKey, - () => new DbContextDatabaseApi( - unitOfWork.ServiceProvider.GetRequiredService() + () => new EfCoreDatabaseApi( + CreateDbContext(unitOfWork, connectionStringName, connectionString) )); - - return ((DbContextDatabaseApi)databaseApi).DbContext; + + return ((EfCoreDatabaseApi)databaseApi).DbContext; + } + + private TDbContext CreateDbContext(IUnitOfWork unitOfWork, string connectionStringName, string connectionString) + { + var creationContext = new DbContextCreationContext(connectionStringName, connectionString); + using (DbContextCreationContext.Use(creationContext)) + { + var dbContext = CreateDbContext(unitOfWork); + + if (unitOfWork.Options.Timeout.HasValue && + dbContext.Database.IsRelational() && + !dbContext.Database.GetCommandTimeout().HasValue) + { + dbContext.Database.SetCommandTimeout(unitOfWork.Options.Timeout.Value.TotalSeconds.To()); + } + + return dbContext; + } + } + + private TDbContext CreateDbContext(IUnitOfWork unitOfWork) + { + return unitOfWork.Options.IsTransactional + ? CreateDbContextWithTransaction(unitOfWork) + : unitOfWork.ServiceProvider.GetRequiredService(); + } + + public TDbContext CreateDbContextWithTransaction(IUnitOfWork unitOfWork) + { + var transactionApiKey = $"EntityFrameworkCore_{DbContextCreationContext.Current.ConnectionString}"; + var activeTransaction = unitOfWork.FindTransactionApi(transactionApiKey) as EfCoreTransactionApi; + + if (activeTransaction == null) + { + var dbContext = unitOfWork.ServiceProvider.GetRequiredService(); + + var dbtransaction = unitOfWork.Options.IsolationLevel.HasValue + ? dbContext.Database.BeginTransaction(unitOfWork.Options.IsolationLevel.Value) + : dbContext.Database.BeginTransaction(); + + unitOfWork.AddTransactionApi( + transactionApiKey, + new EfCoreTransactionApi( + dbtransaction, + dbContext + ) + ); + + return dbContext; + } + else + { + DbContextCreationContext.Current.ExistingConnection = activeTransaction.DbContextTransaction.GetDbTransaction().Connection; + + var dbContext = unitOfWork.ServiceProvider.GetRequiredService(); + + if (dbContext.HasRelationalTransactionManager()) + { + dbContext.Database.UseTransaction(activeTransaction.DbContextTransaction.GetDbTransaction()); + } + else + { + dbContext.Database.BeginTransaction(); //TODO: Why not using the new created transaction? + } + + activeTransaction.AttendedDbContexts.Add(dbContext); + + return dbContext; + } } } } \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/AbpApplicationBase.cs b/src/Volo.Abp/Volo/Abp/AbpApplicationBase.cs index a969b2ab97..d64e415e7f 100644 --- a/src/Volo.Abp/Volo/Abp/AbpApplicationBase.cs +++ b/src/Volo.Abp/Volo/Abp/AbpApplicationBase.cs @@ -39,6 +39,18 @@ namespace Volo.Abp Modules = LoadModules(services, options); } + public virtual void Shutdown() + { + ServiceProvider + .GetRequiredService() + .ShutdownModules(new ApplicationShutdownContext()); + } + + public virtual void Dispose() + { + + } + private IReadOnlyList LoadModules(IServiceCollection services, AbpApplicationCreationOptions options) { return services @@ -50,16 +62,14 @@ namespace Volo.Abp ); } - public virtual void Shutdown() - { - ServiceProvider - .GetRequiredService() - .ShutdownModules(new ApplicationShutdownContext()); - } - - public virtual void Dispose() + protected virtual void InitializeModules() { - + using (var scope = ServiceProvider.CreateScope()) + { + ServiceProvider + .GetRequiredService() + .InitializeModules(new ApplicationInitializationContext(scope.ServiceProvider)); + } } } } \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/AbpApplicationWithExternalServiceProvider.cs b/src/Volo.Abp/Volo/Abp/AbpApplicationWithExternalServiceProvider.cs index 6656baaa5b..ea846809fa 100644 --- a/src/Volo.Abp/Volo/Abp/AbpApplicationWithExternalServiceProvider.cs +++ b/src/Volo.Abp/Volo/Abp/AbpApplicationWithExternalServiceProvider.cs @@ -1,7 +1,6 @@ using System; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.Modularity; namespace Volo.Abp { @@ -25,12 +24,7 @@ namespace Volo.Abp ServiceProvider = serviceProvider; - using (var scope = ServiceProvider.CreateScope()) - { - ServiceProvider - .GetRequiredService() - .InitializeModules(new ApplicationInitializationContext(scope.ServiceProvider)); - } + InitializeModules(); } } } diff --git a/src/Volo.Abp/Volo/Abp/AbpApplicationWithInternalServiceProvider.cs b/src/Volo.Abp/Volo/Abp/AbpApplicationWithInternalServiceProvider.cs index 82c1718dd7..da27610c4a 100644 --- a/src/Volo.Abp/Volo/Abp/AbpApplicationWithInternalServiceProvider.cs +++ b/src/Volo.Abp/Volo/Abp/AbpApplicationWithInternalServiceProvider.cs @@ -1,7 +1,6 @@ using System; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.Modularity; namespace Volo.Abp { @@ -37,12 +36,7 @@ namespace Volo.Abp ServiceScope = Services.BuildServiceProviderFromFactory().CreateScope(); ServiceProvider = ServiceScope.ServiceProvider; - using (var scope = ServiceProvider.CreateScope()) - { - ServiceProvider - .GetRequiredService() - .InitializeModules(new ApplicationInitializationContext(scope.ServiceProvider)); - } + InitializeModules(); } public override void Dispose() diff --git a/src/Volo.Abp/Volo/Abp/AbpKernelModule.cs b/src/Volo.Abp/Volo/Abp/AbpKernelModule.cs index 702155a3e1..3f3e42aadf 100644 --- a/src/Volo.Abp/Volo/Abp/AbpKernelModule.cs +++ b/src/Volo.Abp/Volo/Abp/AbpKernelModule.cs @@ -37,6 +37,14 @@ namespace Volo.Abp services.AddAssemblyOf(); services.TryAddObjectAccessor(); + + services.Configure(options => + { + options.Contributers.Add(); + options.Contributers.Add(); + options.Contributers.Add(); + options.Contributers.Add(); + }); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/src/Volo.Abp/Volo/Abp/AbpServiceBase.cs b/src/Volo.Abp/Volo/Abp/AbpServiceBase.cs index 097e60aea4..7911c7a366 100644 --- a/src/Volo.Abp/Volo/Abp/AbpServiceBase.cs +++ b/src/Volo.Abp/Volo/Abp/AbpServiceBase.cs @@ -15,10 +15,10 @@ namespace Volo.Abp public IGuidGenerator GuidGenerator { get; set; } - protected IUnitOfWork CurrentUnitOfWork => UnitOfWorkManager?.Current; - public ILoggerFactory LoggerFactory { get; set; } + protected IUnitOfWork CurrentUnitOfWork => UnitOfWorkManager?.Current; + protected ILogger Logger => _lazyLogger.Value; private Lazy _lazyLogger => new Lazy(() => LoggerFactory?.CreateLogger(GetType().FullName) ?? NullLogger.Instance, true); diff --git a/src/Volo.Abp/Volo/Abp/Modularity/AbpModule.cs b/src/Volo.Abp/Volo/Abp/Modularity/AbpModule.cs index fab912f73e..11f2cb3f34 100644 --- a/src/Volo.Abp/Volo/Abp/Modularity/AbpModule.cs +++ b/src/Volo.Abp/Volo/Abp/Modularity/AbpModule.cs @@ -4,7 +4,14 @@ using Microsoft.Extensions.DependencyInjection; namespace Volo.Abp.Modularity { - public abstract class AbpModule : IAbpModule, IOnApplicationInitialization, IOnApplicationShutdown, IPreConfigureServices, IPostConfigureServices + public abstract class AbpModule : + IAbpModule, + IOnPreApplicationInitialization, + IOnApplicationInitialization, + IOnPostApplicationInitialization, + IOnApplicationShutdown, + IPreConfigureServices, + IPostConfigureServices { public virtual void PreConfigureServices(IServiceCollection services) { @@ -21,11 +28,21 @@ namespace Volo.Abp.Modularity } + public virtual void OnPreApplicationInitialization(ApplicationInitializationContext context) + { + + } + public virtual void OnApplicationInitialization(ApplicationInitializationContext context) { } + public virtual void OnPostApplicationInitialization(ApplicationInitializationContext context) + { + + } + public virtual void OnApplicationShutdown(ApplicationShutdownContext context) { diff --git a/src/Volo.Abp/Volo/Abp/Modularity/DefaultModuleLifecycleContributer.cs b/src/Volo.Abp/Volo/Abp/Modularity/DefaultModuleLifecycleContributer.cs index 01d7181cbd..063fe0cf12 100644 --- a/src/Volo.Abp/Volo/Abp/Modularity/DefaultModuleLifecycleContributer.cs +++ b/src/Volo.Abp/Volo/Abp/Modularity/DefaultModuleLifecycleContributer.cs @@ -1,15 +1,34 @@ namespace Volo.Abp.Modularity { - public class DefaultModuleLifecycleContributer : ModuleLifecycleContributerBase + public class OnApplicationInitializationModuleLifecycleContributer : ModuleLifecycleContributerBase { public override void Initialize(ApplicationInitializationContext context, IAbpModule module) { (module as IOnApplicationInitialization)?.OnApplicationInitialization(context); } + } + public class OnApplicationShutdownModuleLifecycleContributer : ModuleLifecycleContributerBase + { public override void Shutdown(ApplicationShutdownContext context, IAbpModule module) { (module as IOnApplicationShutdown)?.OnApplicationShutdown(context); } } + + public class OnPreApplicationInitializationModuleLifecycleContributer : ModuleLifecycleContributerBase + { + public override void Initialize(ApplicationInitializationContext context, IAbpModule module) + { + (module as IOnPreApplicationInitialization)?.OnPreApplicationInitialization(context); + } + } + + public class OnPostApplicationInitializationModuleLifecycleContributer : ModuleLifecycleContributerBase + { + public override void Initialize(ApplicationInitializationContext context, IAbpModule module) + { + (module as IOnPostApplicationInitialization)?.OnPostApplicationInitialization(context); + } + } } \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Modularity/IOnPostApplicationInitialization.cs b/src/Volo.Abp/Volo/Abp/Modularity/IOnPostApplicationInitialization.cs new file mode 100644 index 0000000000..a63def315a --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Modularity/IOnPostApplicationInitialization.cs @@ -0,0 +1,9 @@ +using JetBrains.Annotations; + +namespace Volo.Abp.Modularity +{ + public interface IOnPostApplicationInitialization + { + void OnPostApplicationInitialization([NotNull] ApplicationInitializationContext context); + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Modularity/IOnPreApplicationInitialization.cs b/src/Volo.Abp/Volo/Abp/Modularity/IOnPreApplicationInitialization.cs new file mode 100644 index 0000000000..a6c30880cd --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Modularity/IOnPreApplicationInitialization.cs @@ -0,0 +1,9 @@ +using JetBrains.Annotations; + +namespace Volo.Abp.Modularity +{ + public interface IOnPreApplicationInitialization + { + void OnPreApplicationInitialization([NotNull] ApplicationInitializationContext context); + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Modularity/ModuleLifecycleOptions.cs b/src/Volo.Abp/Volo/Abp/Modularity/ModuleLifecycleOptions.cs new file mode 100644 index 0000000000..75f53357b4 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Modularity/ModuleLifecycleOptions.cs @@ -0,0 +1,14 @@ +using Volo.Abp.Collections; + +namespace Volo.Abp.Modularity +{ + public class ModuleLifecycleOptions + { + public ITypeList Contributers { get; } + + public ModuleLifecycleOptions() + { + Contributers = new TypeList(); + } + } +} diff --git a/src/Volo.Abp/Volo/Abp/Modularity/ModuleManager.cs b/src/Volo.Abp/Volo/Abp/Modularity/ModuleManager.cs index 5644693da1..005fe09cff 100644 --- a/src/Volo.Abp/Volo/Abp/Modularity/ModuleManager.cs +++ b/src/Volo.Abp/Volo/Abp/Modularity/ModuleManager.cs @@ -1,5 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; namespace Volo.Abp.Modularity @@ -12,12 +16,18 @@ namespace Volo.Abp.Modularity public ModuleManager( IModuleContainer moduleContainer, - IEnumerable lifecycleContributers, - ILogger logger) + ILogger logger, + IOptions options, + IServiceProvider serviceProvider) { _moduleContainer = moduleContainer; - _lifecycleContributers = lifecycleContributers; _logger = logger; + + _lifecycleContributers = options.Value + .Contributers + .Select(serviceProvider.GetRequiredService) + .Cast() + .ToArray(); } public void InitializeModules(ApplicationInitializationContext context) diff --git a/src/Volo.Abp/Volo/Abp/Uow/AmbientUnitOfWork.cs b/src/Volo.Abp/Volo/Abp/Uow/AmbientUnitOfWork.cs index 6c01daf350..7b3a45b20c 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/AmbientUnitOfWork.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/AmbientUnitOfWork.cs @@ -6,18 +6,18 @@ namespace Volo.Abp.Uow [ExposeServices(typeof(IAmbientUnitOfWork), typeof(IUnitOfWorkAccessor))] public class AmbientUnitOfWork : IAmbientUnitOfWork, ISingletonDependency { - public IUnitOfWork UnitOfWork => _currentUowInfo.Value; + public IUnitOfWork UnitOfWork => _currentUow.Value; - private readonly AsyncLocal _currentUowInfo; + private readonly AsyncLocal _currentUow; public AmbientUnitOfWork() { - _currentUowInfo = new AsyncLocal(); + _currentUow = new AsyncLocal(); } public void SetUnitOfWork(IUnitOfWork unitOfWork) { - _currentUowInfo.Value = unitOfWork; + _currentUow.Value = unitOfWork; } } } \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/ChildUnitOfWork.cs b/src/Volo.Abp/Volo/Abp/Uow/ChildUnitOfWork.cs index 47dc909581..4fc789c6ae 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/ChildUnitOfWork.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/ChildUnitOfWork.cs @@ -7,9 +7,19 @@ namespace Volo.Abp.Uow { internal class ChildUnitOfWork : IUnitOfWork { - public event EventHandler Completed; + public Guid Id => _parent.Id; + + public IUnitOfWorkOptions Options => _parent.Options; + + public IUnitOfWork Outer => _parent.Outer; + + public bool IsReserved => _parent.IsReserved; + + public string ReservationName => _parent.ReservationName; + + public event EventHandler Completed; public event EventHandler Failed; - public event EventHandler Disposed; + public event EventHandler Disposed; public IServiceProvider ServiceProvider => _parent.ServiceProvider; @@ -26,6 +36,21 @@ namespace Volo.Abp.Uow _parent.Disposed += (sender, args) => { Disposed.InvokeSafely(sender, args); }; } + public void SetOuter(IUnitOfWork outer) + { + _parent.SetOuter(outer); + } + + public void Initialize(UnitOfWorkOptions options) + { + _parent.Initialize(options); + } + + public void Reserve(string reservationName) + { + _parent.Reserve(reservationName); + } + public void SaveChanges() { _parent.SaveChanges(); @@ -35,30 +60,65 @@ namespace Volo.Abp.Uow { return _parent.SaveChangesAsync(cancellationToken); } - + public void Complete() { - + } public Task CompleteAsync(CancellationToken cancellationToken = default(CancellationToken)) { return Task.CompletedTask; } - - public IDatabaseApi FindDatabaseApi(string id) + + public void Rollback() + { + _parent.Rollback(); + } + + public Task RollbackAsync(CancellationToken cancellationToken = default(CancellationToken)) { - return _parent.FindDatabaseApi(id); + return _parent.RollbackAsync(cancellationToken); } - public IDatabaseApi GetOrAddDatabaseApi(string id, Func factory) + public IDatabaseApi FindDatabaseApi(string key) { - return _parent.GetOrAddDatabaseApi(id, factory); + return _parent.FindDatabaseApi(key); } - + + public void AddDatabaseApi(string key, IDatabaseApi api) + { + _parent.AddDatabaseApi(key, api); + } + + public IDatabaseApi GetOrAddDatabaseApi(string key, Func factory) + { + 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 ITransactionApi GetOrAddTransactionApi(string key, Func factory) + { + return _parent.GetOrAddTransactionApi(key, factory); + } + public void Dispose() { } + + public override string ToString() + { + return $"[UnitOfWork {Id}]"; + } } } \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/IBasicUnitOfWork.cs b/src/Volo.Abp/Volo/Abp/Uow/IBasicUnitOfWork.cs deleted file mode 100644 index 60038cae2b..0000000000 --- a/src/Volo.Abp/Volo/Abp/Uow/IBasicUnitOfWork.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Volo.Abp.Uow -{ - //TODO: Find a better naming :( - public interface IBasicUnitOfWork : IDisposable - { - event EventHandler Completed; - - event EventHandler Failed; - - event EventHandler Disposed; - - void SaveChanges(); - - Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)); - - void Complete(); - - Task CompleteAsync(CancellationToken cancellationToken = default(CancellationToken)); - } -} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/IDatabaseApiContainer.cs b/src/Volo.Abp/Volo/Abp/Uow/IDatabaseApiContainer.cs index 536823fd57..5ee4ff5f41 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/IDatabaseApiContainer.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/IDatabaseApiContainer.cs @@ -7,9 +7,11 @@ namespace Volo.Abp.Uow public interface IDatabaseApiContainer : IServiceProviderAccessor { [CanBeNull] - IDatabaseApi FindDatabaseApi([NotNull] string id); + IDatabaseApi FindDatabaseApi([NotNull] string key); + + void AddDatabaseApi([NotNull] string key, [NotNull] IDatabaseApi api); [NotNull] - IDatabaseApi GetOrAddDatabaseApi(string id, Func factory); + IDatabaseApi GetOrAddDatabaseApi([NotNull] string key, [NotNull] Func factory); } } \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/ISupportsRollback.cs b/src/Volo.Abp/Volo/Abp/Uow/ISupportsRollback.cs new file mode 100644 index 0000000000..4c211d652d --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Uow/ISupportsRollback.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Volo.Abp.Uow +{ + public interface ISupportsRollback + { + void Rollback(); + + Task RollbackAsync(CancellationToken cancellationToken); + } +} \ No newline at end of file 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..b3dd050dc5 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Uow/ITransactionApiContainer.cs @@ -0,0 +1,16 @@ +using System; +using JetBrains.Annotations; + +namespace Volo.Abp.Uow +{ + public interface ITransactionApiContainer + { + [CanBeNull] + ITransactionApi FindTransactionApi([NotNull] string key); + + void AddTransactionApi([NotNull] string key, [NotNull] ITransactionApi api); + + [NotNull] + ITransactionApi GetOrAddTransactionApi([NotNull] string key, [NotNull] Func factory); + } +} \ 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 01d6baeb2f..79089cd8fc 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWork.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWork.cs @@ -1,7 +1,44 @@ -namespace Volo.Abp.Uow +using System; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Volo.Abp.Uow { - public interface IUnitOfWork : IBasicUnitOfWork, IDatabaseApiContainer + public interface IUnitOfWork : IDatabaseApiContainer, ITransactionApiContainer, IDisposable { - + Guid Id { get; } + + event EventHandler Completed; + + event EventHandler Failed; + + event EventHandler Disposed; + + IUnitOfWorkOptions Options { get; } + + IUnitOfWork Outer { get; } + + bool IsReserved { get; } + + string ReservationName { get; } + + void SetOuter([CanBeNull] IUnitOfWork outer); + + void Initialize([NotNull] UnitOfWorkOptions options); + + void Reserve([NotNull] string reservationName); + + void SaveChanges(); + + Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)); + + void Complete(); + + Task CompleteAsync(CancellationToken cancellationToken = default(CancellationToken)); + + void Rollback(); + + Task RollbackAsync(CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWorkManager.cs b/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWorkManager.cs index e03e069535..264390fbbe 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWorkManager.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWorkManager.cs @@ -8,6 +8,13 @@ namespace Volo.Abp.Uow IUnitOfWork Current { get; } [NotNull] - IBasicUnitOfWork Begin(); + IUnitOfWork Begin([NotNull] UnitOfWorkOptions options, bool requiresNew = false); + + [NotNull] + IUnitOfWork Reserve([NotNull] string reservationName, bool requiresNew = false); + + void BeginReserved([NotNull] string reservationName, [NotNull] UnitOfWorkOptions options); + + bool TryBeginReserved([NotNull] string reservationName, [NotNull] UnitOfWorkOptions options); } } \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWorkOptions.cs b/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWorkOptions.cs new file mode 100644 index 0000000000..b79b7112b8 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWorkOptions.cs @@ -0,0 +1,14 @@ +using System; +using System.Data; + +namespace Volo.Abp.Uow +{ + public interface IUnitOfWorkOptions + { + bool IsTransactional { get; } + + IsolationLevel? IsolationLevel { get; } + + TimeSpan? Timeout { get; } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWork.cs b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWork.cs index a94faba0d7..aac08f3fa2 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWork.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWork.cs @@ -2,46 +2,71 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; namespace Volo.Abp.Uow { public class UnitOfWork : IUnitOfWork, ITransientDependency { - public event EventHandler Completed; + public Guid Id { get; } = Guid.NewGuid(); + + public IUnitOfWorkOptions Options { get; private set; } + + public IUnitOfWork Outer { get; private set; } + + public bool IsReserved { get; set; } + + public string ReservationName { get; set; } + + public event EventHandler Completed; public event EventHandler Failed; - public event EventHandler Disposed; + public event EventHandler Disposed; public IServiceProvider ServiceProvider { get; } private readonly Dictionary _databaseApis; + private readonly Dictionary _transactionApis; + private readonly UnitOfWorkDefaultOptions _defaultOptions; private Exception _exception; private bool _isCompleted; private bool _isDisposed; + private bool _isRolledback; - public UnitOfWork(IServiceProvider serviceProvider) + public UnitOfWork(IServiceProvider serviceProvider, IOptions options) { ServiceProvider = serviceProvider; + _defaultOptions = options.Value; _databaseApis = new Dictionary(); + _transactionApis = new Dictionary(); } - public void Dispose() + public void Initialize(UnitOfWorkOptions options) { - if (_isDisposed) + Check.NotNull(options, nameof(options)); + + if (Options != null) { - return; + throw new AbpException("This unit of work is already initialized before!"); } - _isDisposed = true; + Options = _defaultOptions.Normalize(options.Clone()); + IsReserved = false; + } - if (!_isCompleted || _exception != null) - { - OnFailed(_exception); - } + public void Reserve(string reservationName) + { + Check.NotNull(reservationName, nameof(reservationName)); - OnDisposed(); + ReservationName = reservationName; + IsReserved = true; + } + + public void SetOuter(IUnitOfWork outer) + { + Outer = outer; } public void SaveChanges() @@ -65,10 +90,17 @@ namespace Volo.Abp.Uow public void Complete() { + if (_isRolledback) + { + return; + } + PreventMultipleComplete(); + try { SaveChanges(); + CommitTransactions(); OnCompleted(); } catch (Exception ex) @@ -80,11 +112,17 @@ namespace Volo.Abp.Uow public async Task CompleteAsync(CancellationToken cancellationToken = default(CancellationToken)) { + if (_isRolledback) + { + return; + } + PreventMultipleComplete(); try { await SaveChangesAsync(cancellationToken); + await CommitTransactionsAsync(); OnCompleted(); } catch (Exception ex) @@ -94,32 +132,136 @@ namespace Volo.Abp.Uow } } - public IDatabaseApi FindDatabaseApi(string id) + public void Rollback() + { + if (_isRolledback) + { + return; + } + + _isRolledback = true; + + foreach (var databaseApi in _databaseApis.Values) + { + (databaseApi as ISupportsRollback)?.Rollback(); + } + + foreach (var transactionApi in _transactionApis.Values) + { + (transactionApi as ISupportsRollback)?.Rollback(); + } + } + + public async Task RollbackAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + if (_isRolledback) + { + return; + } + + _isRolledback = true; + + foreach (var databaseApi in _databaseApis.Values) + { + if (databaseApi is ISupportsRollback) + { + await (databaseApi as ISupportsRollback).RollbackAsync(cancellationToken); + } + } + + foreach (var transactionApi in _transactionApis.Values) + { + if (transactionApi is ISupportsRollback) + { + await (transactionApi as ISupportsRollback).RollbackAsync(cancellationToken); + } + } + } + + public IDatabaseApi FindDatabaseApi(string key) + { + return _databaseApis.GetOrDefault(key); + } + + public void AddDatabaseApi(string key, IDatabaseApi api) + { + Check.NotNull(key, nameof(key)); + Check.NotNull(api, nameof(api)); + + if (_databaseApis.ContainsKey(key)) + { + throw new AbpException("There is already a database API in this unit of work with given key: " + key); + } + + _databaseApis.Add(key, api); + } + + public IDatabaseApi GetOrAddDatabaseApi(string key, Func factory) { - return _databaseApis.GetOrDefault(id); + Check.NotNull(key, nameof(key)); + Check.NotNull(factory, nameof(factory)); + + 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 with given key: " + key); + } + + _transactionApis.Add(key, api); } - public IDatabaseApi GetOrAddDatabaseApi(string id, Func factory) + public ITransactionApi GetOrAddTransactionApi(string key, Func factory) { - Check.NotNull(id, nameof(id)); + Check.NotNull(key, nameof(key)); Check.NotNull(factory, nameof(factory)); - return _databaseApis.GetOrAdd(id, factory); + return _transactionApis.GetOrAdd(key, factory); } protected virtual void OnCompleted() { - Completed.InvokeSafely(this); + Completed.InvokeSafely(this, new UnitOfWorkEventArgs(this)); } - protected virtual void OnFailed(Exception exception) + protected virtual void OnFailed() { - Failed.InvokeSafely(this, new UnitOfWorkFailedEventArgs(exception)); + Failed.InvokeSafely(this, new UnitOfWorkFailedEventArgs(this, _exception, _isRolledback)); } protected virtual void OnDisposed() { - Disposed.InvokeSafely(this); + Disposed.InvokeSafely(this, new UnitOfWorkEventArgs(this)); + } + + public void Dispose() + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + if (!_isCompleted || _exception != null) + { + OnFailed(); + } + + OnDisposed(); } private void PreventMultipleComplete() @@ -131,5 +273,26 @@ namespace Volo.Abp.Uow _isCompleted = true; } + + public override string ToString() + { + 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 diff --git a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkAttribute.cs b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkAttribute.cs index 1425d7c263..351eefd859 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkAttribute.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Data; namespace Volo.Abp.Uow { @@ -11,5 +12,47 @@ namespace Volo.Abp.Uow [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Interface)] public class UnitOfWorkAttribute : Attribute { + /// + /// Is this UOW transactional? + /// Uses default value if not supplied. + /// + public bool? IsTransactional { get; set; } + + /// + /// Timeout of UOW As milliseconds. + /// Uses default value if not supplied. + /// + public TimeSpan? Timeout { get; set; } + + /// + /// If this UOW is transactional, this option indicated the isolation level of the transaction. + /// Uses default value if not supplied. + /// + public IsolationLevel? IsolationLevel { get; set; } + + /// + /// Used to prevent starting a unit of work for the method. + /// If there is already a started unit of work, this property is ignored. + /// Default: false. + /// + public bool IsDisabled { get; set; } + + public virtual void SetOptions(UnitOfWorkOptions options) + { + if (IsTransactional.HasValue) + { + options.IsTransactional = IsTransactional.Value; + } + + if (Timeout.HasValue) + { + options.Timeout = Timeout; + } + + if (IsolationLevel.HasValue) + { + options.IsolationLevel = IsolationLevel; + } + } } } diff --git a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkDefaultOptions.cs b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkDefaultOptions.cs new file mode 100644 index 0000000000..cb7288549d --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkDefaultOptions.cs @@ -0,0 +1,50 @@ +using System; +using System.Data; +using System.Reflection; + +namespace Volo.Abp.Uow +{ + //TODO: Implement default options! + + /// + /// Global (default) unit of work options + /// + public class UnitOfWorkDefaultOptions + { + public UnitOfWorkTransactionBehavior TransactionBehavior { get; set; } + + public IsolationLevel? IsolationLevel { get; set; } + + public TimeSpan? Timeout { get; set; } + + internal UnitOfWorkOptions Normalize(UnitOfWorkOptions options) + { + if (options.IsolationLevel == null) + { + options.IsolationLevel = IsolationLevel; + } + + if (options.Timeout == null) + { + options.Timeout = Timeout; + } + + return options; + } + + internal bool CalculateIsTransactional(bool autoValue) + { + switch (TransactionBehavior) + { + case UnitOfWorkTransactionBehavior.Enabled: + return true; + case UnitOfWorkTransactionBehavior.Disabled: + return false; + case UnitOfWorkTransactionBehavior.Auto: + return autoValue; + default: + throw new AbpException("Not implemented TransactionBehavior value: " + TransactionBehavior); + } + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkEventArgs.cs b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkEventArgs.cs new file mode 100644 index 0000000000..9ab7c953f9 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkEventArgs.cs @@ -0,0 +1,20 @@ +using System; +using JetBrains.Annotations; + +namespace Volo.Abp.Uow +{ + public class UnitOfWorkEventArgs : EventArgs + { + /// + /// Reference to the unit of work related to this event. + /// + public IUnitOfWork UnitOfWork { get; } + + public UnitOfWorkEventArgs([NotNull] IUnitOfWork unitOfWork) + { + Check.NotNull(unitOfWork, nameof(unitOfWork)); + + UnitOfWork = unitOfWork; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkExtensions.cs b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkExtensions.cs new file mode 100644 index 0000000000..5c01f54a99 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkExtensions.cs @@ -0,0 +1,14 @@ +using JetBrains.Annotations; + +namespace Volo.Abp.Uow +{ + public static class UnitOfWorkExtensions + { + public static bool IsReservedFor([NotNull] this IUnitOfWork unitOfWork, string reservationName) + { + Check.NotNull(unitOfWork, nameof(unitOfWork)); + + return unitOfWork.IsReserved && unitOfWork.ReservationName == reservationName; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkFailedEventArgs.cs b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkFailedEventArgs.cs index 870f125f4f..4218a9693f 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkFailedEventArgs.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkFailedEventArgs.cs @@ -6,23 +6,29 @@ namespace Volo.Abp.Uow /// /// Used as event arguments on event. /// - public class UnitOfWorkFailedEventArgs : EventArgs + public class UnitOfWorkFailedEventArgs : UnitOfWorkEventArgs { /// - /// Exception that caused failure. This is set only if an error occured during . - /// Can be null if there is no exception, but is not called. + /// Exception that caused failure. This is set only if an error occured during . + /// Can be null if there is no exception, but is not called. /// Can be null if another exception occurred during the UOW. /// [CanBeNull] - public Exception Exception { get; private set; } + public Exception Exception { get; } + + /// + /// True, if the unit of work is manually rolled back. + /// + public bool IsRolledback { get; } /// /// Creates a new object. /// - /// Exception that caused failure - public UnitOfWorkFailedEventArgs([CanBeNull] Exception exception) + public UnitOfWorkFailedEventArgs([NotNull] IUnitOfWork unitOfWork, [CanBeNull] Exception exception, bool isRolledback) + : base(unitOfWork) { Exception = exception; + IsRolledback = isRolledback; } } } diff --git a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkHelper.cs b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkHelper.cs index fe8d91faef..47d42aa322 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkHelper.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkHelper.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Reflection; +using JetBrains.Annotations; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; @@ -25,13 +26,16 @@ namespace Volo.Abp.Uow return false; } - public static bool IsUnitOfWorkMethod(MethodInfo methodInfo) + public static bool IsUnitOfWorkMethod([NotNull] MethodInfo methodInfo, [CanBeNull] out UnitOfWorkAttribute unitOfWorkAttribute) { + Check.NotNull(methodInfo, nameof(methodInfo)); + //Method declaration var attrs = methodInfo.GetCustomAttributes(true).OfType().ToArray(); if (attrs.Any()) { - return true; + unitOfWorkAttribute = attrs.First(); + return !unitOfWorkAttribute.IsDisabled; } if (methodInfo.DeclaringType != null) @@ -40,20 +44,41 @@ namespace Volo.Abp.Uow attrs = methodInfo.DeclaringType.GetTypeInfo().GetCustomAttributes(true).OfType().ToArray(); if (attrs.Any()) { - return true; + unitOfWorkAttribute = attrs.First(); + return !unitOfWorkAttribute.IsDisabled; } - //Conventional classes + //Conventional classes //TODO: Make this extendible to add new conventions! if (typeof(IApplicationService).GetTypeInfo().IsAssignableFrom(methodInfo.DeclaringType) || typeof(IRepository).GetTypeInfo().IsAssignableFrom(methodInfo.DeclaringType)) { + unitOfWorkAttribute = null; return true; } } + unitOfWorkAttribute = null; return false; } + public static UnitOfWorkAttribute GetUnitOfWorkAttributeOrNull(MethodInfo methodInfo) + { + var attrs = methodInfo.GetCustomAttributes(true).OfType().ToArray(); + if (attrs.Length > 0) + { + return attrs[0]; + } + + attrs = methodInfo.DeclaringType.GetTypeInfo().GetCustomAttributes(true).OfType().ToArray(); + if (attrs.Length > 0) + { + return attrs[0]; + } + + return null; + } + + private static bool AnyMethodHasUnitOfWorkAttribute(TypeInfo implementationType) { return implementationType diff --git a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkInterceptor.cs b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkInterceptor.cs index 1bf813905a..b95173edde 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkInterceptor.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkInterceptor.cs @@ -1,4 +1,8 @@ -using System.Threading.Tasks; +using System; +using System.Net.Http; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; using Volo.Abp.DynamicProxy; @@ -7,40 +11,55 @@ namespace Volo.Abp.Uow public class UnitOfWorkInterceptor : AbpInterceptor, ITransientDependency { private readonly IUnitOfWorkManager _unitOfWorkManager; + private readonly UnitOfWorkDefaultOptions _defaultOptions; - public UnitOfWorkInterceptor(IUnitOfWorkManager unitOfWorkManager) + public UnitOfWorkInterceptor(IUnitOfWorkManager unitOfWorkManager, IOptions options) { _unitOfWorkManager = unitOfWorkManager; + _defaultOptions = options.Value; } public override void Intercept(IAbpMethodInvocation invocation) { - if (!UnitOfWorkHelper.IsUnitOfWorkMethod(invocation.Method)) + if (!UnitOfWorkHelper.IsUnitOfWorkMethod(invocation.Method, out var unitOfWorkAttribute)) { invocation.Proceed(); return; } - using (var uow = _unitOfWorkManager.Begin()) + using (var uow = _unitOfWorkManager.Begin(CreateOptions(invocation, unitOfWorkAttribute))) { invocation.Proceed(); uow.Complete(); } } - public override async Task InterceptAsync(IAbpMethodInvocation invocation) + public override async Task InterceptAsync(IAbpMethodInvocation invocation) { - if (!UnitOfWorkHelper.IsUnitOfWorkMethod(invocation.Method)) + if (!UnitOfWorkHelper.IsUnitOfWorkMethod(invocation.Method, out var unitOfWorkAttribute)) { await invocation.ProceedAsync(); return; } - using (var uow = _unitOfWorkManager.Begin()) + using (var uow = _unitOfWorkManager.Begin(CreateOptions(invocation, unitOfWorkAttribute))) { await invocation.ProceedAsync(); await uow.CompleteAsync(); } } + + private UnitOfWorkOptions CreateOptions(IAbpMethodInvocation invocation, [CanBeNull] UnitOfWorkAttribute unitOfWorkAttribute) + { + var options = new UnitOfWorkOptions(); + + unitOfWorkAttribute?.SetOptions(options); + + options.IsTransactional = _defaultOptions.CalculateIsTransactional( + autoValue: !invocation.Method.Name.StartsWith("Get", StringComparison.InvariantCultureIgnoreCase) + ); + + return options; + } } } diff --git a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkManager.cs b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkManager.cs index 534b1c0db8..447a6c182e 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkManager.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkManager.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.DependencyInjection; @@ -7,61 +6,120 @@ namespace Volo.Abp.Uow { public class UnitOfWorkManager : IUnitOfWorkManager, ISingletonDependency { - public IUnitOfWork Current => _ambientUnitOfWork.UnitOfWork; + public IUnitOfWork Current => GetCurrentUnitOfWork(); private readonly IServiceProvider _serviceProvider; private readonly IAmbientUnitOfWork _ambientUnitOfWork; - public UnitOfWorkManager(IServiceProvider serviceProvider, IAmbientUnitOfWork ambientUnitOfWork) + public UnitOfWorkManager( + IServiceProvider serviceProvider, + IAmbientUnitOfWork ambientUnitOfWork) { _serviceProvider = serviceProvider; _ambientUnitOfWork = ambientUnitOfWork; } - public IBasicUnitOfWork Begin() + public IUnitOfWork Begin(UnitOfWorkOptions options, bool requiresNew = false) { - if (_ambientUnitOfWork.UnitOfWork != null) + Check.NotNull(options, nameof(options)); + + if (!requiresNew && _ambientUnitOfWork.UnitOfWork != null && !_ambientUnitOfWork.UnitOfWork.IsReserved) { return new ChildUnitOfWork(_ambientUnitOfWork.UnitOfWork); } - var parentUow = _ambientUnitOfWork.UnitOfWork; + var unitOfWork = CreateNewUnitOfWork(); + unitOfWork.Initialize(options); - var scope = _serviceProvider.CreateScope(); - IUnitOfWork unitOfWork; - try + return unitOfWork; + } + + public IUnitOfWork Reserve(string reservationName, bool requiresNew = false) + { + Check.NotNull(reservationName, nameof(reservationName)); + + if (!requiresNew && + _ambientUnitOfWork.UnitOfWork != null && + _ambientUnitOfWork.UnitOfWork.IsReservedFor(reservationName)) { - unitOfWork = scope.ServiceProvider.GetRequiredService(); + return new ChildUnitOfWork(_ambientUnitOfWork.UnitOfWork); } - catch + + var unitOfWork = CreateNewUnitOfWork(); + unitOfWork.Reserve(reservationName); + + return unitOfWork; + } + + public void BeginReserved(string reservationName, UnitOfWorkOptions options) + { + if (!TryBeginReserved(reservationName, options)) { - scope.Dispose(); - throw; + throw new AbpException($"Could not find a reserved unit of work with reservation name: {reservationName}"); + } + } + + public bool TryBeginReserved(string reservationName, UnitOfWorkOptions options) + { + Check.NotNull(reservationName, nameof(reservationName)); + + var uow = _ambientUnitOfWork.UnitOfWork; + + //Find reserved unit of work starting from current and going to outers + while (uow != null && !uow.IsReservedFor(reservationName)) + { + uow = uow.Outer; + } + + if (uow == null) + { + return false; } - _ambientUnitOfWork.SetUnitOfWork(unitOfWork); + uow.Initialize(options); - Debug.Assert( - _ambientUnitOfWork.UnitOfWork != null, - "_ambientUnitOfWork.UnitOfWork can not be null since it's set by _ambientUnitOfWork.SetUnitOfWork method!" - ); + return true; + } - unitOfWork.Completed += (sender, args) => + private IUnitOfWork GetCurrentUnitOfWork() + { + var uow = _ambientUnitOfWork.UnitOfWork; + + //Skip reserved unit of work + while (uow != null && uow.IsReserved) { - _ambientUnitOfWork.SetUnitOfWork(parentUow); - }; + uow = uow.Outer; + } - unitOfWork.Failed += (sender, args) => + return uow; + } + + private IUnitOfWork CreateNewUnitOfWork() + { + var scope = _serviceProvider.CreateScope(); + try { - _ambientUnitOfWork.SetUnitOfWork(parentUow); - }; + var outerUow = _ambientUnitOfWork.UnitOfWork; + + var unitOfWork = scope.ServiceProvider.GetRequiredService(); + + unitOfWork.SetOuter(outerUow); + + _ambientUnitOfWork.SetUnitOfWork(unitOfWork); + + unitOfWork.Disposed += (sender, args) => + { + _ambientUnitOfWork.SetUnitOfWork(outerUow); + scope.Dispose(); + }; - unitOfWork.Disposed += (sender, args) => + return unitOfWork; + } + catch { scope.Dispose(); - }; - - return _ambientUnitOfWork.UnitOfWork; + throw; + } } } } \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkManagerExtensions.cs b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkManagerExtensions.cs new file mode 100644 index 0000000000..c9f888f275 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkManagerExtensions.cs @@ -0,0 +1,31 @@ +using JetBrains.Annotations; + +namespace Volo.Abp.Uow +{ + public static class UnitOfWorkManagerExtensions + { + [NotNull] + public static IUnitOfWork Begin([NotNull] this IUnitOfWorkManager unitOfWorkManager, bool requiresNew = false) + { + Check.NotNull(unitOfWorkManager, nameof(unitOfWorkManager)); + + return unitOfWorkManager.Begin(new UnitOfWorkOptions(), requiresNew); + } + + public static void BeginReserved([NotNull] this IUnitOfWorkManager unitOfWorkManager, [NotNull] string reservationName) + { + Check.NotNull(unitOfWorkManager, nameof(unitOfWorkManager)); + Check.NotNull(reservationName, nameof(reservationName)); + + unitOfWorkManager.BeginReserved(reservationName, new UnitOfWorkOptions()); + } + + public static void TryBeginReserved([NotNull] this IUnitOfWorkManager unitOfWorkManager, [NotNull] string reservationName) + { + Check.NotNull(unitOfWorkManager, nameof(unitOfWorkManager)); + Check.NotNull(reservationName, nameof(reservationName)); + + unitOfWorkManager.TryBeginReserved(reservationName, new UnitOfWorkOptions()); + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkOptions.cs b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkOptions.cs new file mode 100644 index 0000000000..3d433f3738 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkOptions.cs @@ -0,0 +1,27 @@ +using System; +using System.Data; + +namespace Volo.Abp.Uow +{ + public class UnitOfWorkOptions : IUnitOfWorkOptions + { + /// + /// Default: false. + /// + public bool IsTransactional { get; set; } + + public IsolationLevel? IsolationLevel { get; set; } + + public TimeSpan? Timeout { get; set; } + + public UnitOfWorkOptions Clone() + { + return new UnitOfWorkOptions + { + IsTransactional = IsTransactional, + IsolationLevel = IsolationLevel, + Timeout = Timeout + }; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkTransactionBehavior.cs b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkTransactionBehavior.cs new file mode 100644 index 0000000000..cabcfd0999 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkTransactionBehavior.cs @@ -0,0 +1,11 @@ +namespace Volo.Abp.Uow +{ + public enum UnitOfWorkTransactionBehavior + { + Auto, + + Enabled, + + Disabled + } +} \ No newline at end of file diff --git a/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/App/AbpAspNetCoreMvcTestModule.cs b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/App/AbpAspNetCoreMvcTestModule.cs index bec293205e..e95ed9d90d 100644 --- a/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/App/AbpAspNetCoreMvcTestModule.cs +++ b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/App/AbpAspNetCoreMvcTestModule.cs @@ -34,6 +34,8 @@ namespace Volo.Abp.AspNetCore.App public override void OnApplicationInitialization(ApplicationInitializationContext context) { var app = context.GetApplicationBuilder(); + + app.UseUnitOfWork(); app.UseMvcWithDefaultRoute(); } } diff --git a/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/App/UnitOfWorkTestController.cs b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/App/UnitOfWorkTestController.cs new file mode 100644 index 0000000000..4a465e16db --- /dev/null +++ b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/App/UnitOfWorkTestController.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; +using Shouldly; +using Volo.Abp.AspNetCore.Mvc; + +namespace Volo.Abp.AspNetCore.App +{ + [Route("api/unitofwork-test")] + public class UnitOfWorkTestController : AbpController + { + [HttpGet] + [Route("ActionRequiresUow")] + public ActionResult ActionRequiresUow() + { + CurrentUnitOfWork.ShouldNotBeNull(); + CurrentUnitOfWork.Options.IsTransactional.ShouldBeFalse(); + + return Content("OK"); + } + + [HttpPost] + [Route("ActionRequiresUowPost")] + public ActionResult ActionRequiresUowPost() + { + CurrentUnitOfWork.ShouldNotBeNull(); + CurrentUnitOfWork.Options.IsTransactional.ShouldBeTrue(); + + return Content("OK"); + } + } +} diff --git a/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkMiddleware_Tests.cs b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkMiddleware_Tests.cs new file mode 100644 index 0000000000..abd666e3f5 --- /dev/null +++ b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkMiddleware_Tests.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.Uow +{ + public class UnitOfWorkMiddleware_Tests : AspNetCoreMvcTestBase + { + [Fact] + public async Task Get_Actions_Should_Not_Be_Transactional() + { + await GetResponseAsStringAsync("/api/unitofwork-test/ActionRequiresUow"); + } + + [Fact] + public async Task Non_Get_Actions_Should_Be_Transactional() + { + var result = await Client.PostAsync("/api/unitofwork-test/ActionRequiresUowPost", null); + result.IsSuccessStatusCode.ShouldBeTrue(); + } + } +} diff --git a/test/Volo.Abp.EntityFrameworkCore.Tests/Volo.Abp.EntityFrameworkCore.Tests.csproj b/test/Volo.Abp.EntityFrameworkCore.Tests/Volo.Abp.EntityFrameworkCore.Tests.csproj new file mode 100644 index 0000000000..4186fa8882 --- /dev/null +++ b/test/Volo.Abp.EntityFrameworkCore.Tests/Volo.Abp.EntityFrameworkCore.Tests.csproj @@ -0,0 +1,30 @@ + + + + netcoreapp2.0 + Volo.Abp.EntityFrameworkCore.Tests + Volo.Abp.EntityFrameworkCore.Tests + true + false + false + false + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs b/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs new file mode 100644 index 0000000000..7f406993c9 --- /dev/null +++ b/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs @@ -0,0 +1,42 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Autofac; +using Volo.Abp.Modularity; +using Volo.Abp.TestApp; +using Volo.Abp.TestApp.EntityFrameworkCore; + +namespace Volo.Abp.EntityFrameworkCore +{ + [DependsOn(typeof(AbpEntityFrameworkCoreModule))] + [DependsOn(typeof(TestAppModule))] + [DependsOn(typeof(AbpAutofacModule))] + public class AbpEntityFrameworkCoreTestModule : AbpModule + { + public override void ConfigureServices(IServiceCollection services) + { + services.AddAssemblyOf(); + + services.AddAbpDbContext(options => + { + options.WithDefaultRepositories(); + }); + + var inMemorySqlite = new SqliteConnection("Data Source=:memory:"); + inMemorySqlite.Open(); + + services.Configure(options => + { + options.Configure(context => + { + context.DbContextOptions.UseSqlite(inMemorySqlite); + }); + }); + } + + public override void OnPreApplicationInitialization(ApplicationInitializationContext context) + { + context.ServiceProvider.GetRequiredService().Database.EnsureCreated(); + } + } +} diff --git a/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/EntityFrameworkCoreTestBase.cs b/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/EntityFrameworkCoreTestBase.cs new file mode 100644 index 0000000000..b773962a16 --- /dev/null +++ b/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/EntityFrameworkCoreTestBase.cs @@ -0,0 +1,12 @@ +using Volo.Abp.TestBase; + +namespace Volo.Abp.EntityFrameworkCore +{ + public abstract class EntityFrameworkCoreTestBase : AbpIntegratedTest + { + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.UseAutofac(); + } + } +} diff --git a/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/Basic_Repository_Tests.cs b/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/Basic_Repository_Tests.cs new file mode 100644 index 0000000000..ada013b3d9 --- /dev/null +++ b/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/Basic_Repository_Tests.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.TestApp.Domain; +using Xunit; + +namespace Volo.Abp.EntityFrameworkCore.Repositories +{ + public class Basic_Repository_Tests : EntityFrameworkCoreTestBase + { + private readonly IRepository _personRepository; + + public Basic_Repository_Tests() + { + _personRepository = ServiceProvider.GetRequiredService>(); + } + + [Fact] + public void GetList() + { + _personRepository.GetList().Any().ShouldBeTrue(); + } + + [Fact] + public async Task InsertAsync() + { + var personId = Guid.NewGuid(); + + await _personRepository.InsertAsync(new Person(personId, "Adam", 42)); + + var person = await _personRepository.FindAsync(personId); + person.ShouldNotBeNull(); + } + } +} diff --git a/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Transaction_Tests.cs b/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Transaction_Tests.cs new file mode 100644 index 0000000000..d13a964fc5 --- /dev/null +++ b/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Transaction_Tests.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.TestApp.Domain; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.EntityFrameworkCore +{ + public class Transaction_Tests : EntityFrameworkCoreTestBase + { + private readonly IRepository _personRepository; + private readonly IUnitOfWorkManager _unitOfWorkManager; + + public Transaction_Tests() + { + _personRepository = ServiceProvider.GetRequiredService>(); + _unitOfWorkManager = ServiceProvider.GetRequiredService(); + } + + [Fact] + public async Task Should_Rollback_Transaction_When_An_Exception_Is_Thrown() + { + var personId = Guid.NewGuid(); + const string exceptionMessage = "thrown to rollback the transaction!"; + + try + { + await WithUnitOfWorkAsync(async () => + { + await _personRepository.InsertAsync(new Person(personId, "Adam", 42)); + throw new Exception(exceptionMessage); + }); + } + catch (Exception e) when (e.Message == exceptionMessage) + { + + } + + var person = await _personRepository.FindAsync(personId); + person.ShouldBeNull(); + } + + [Fact] + public async Task Should_Rollback_Transaction_Manually() + { + var personId = Guid.NewGuid(); + + await WithUnitOfWorkAsync(async () => + { + _unitOfWorkManager.Current.ShouldNotBeNull(); + await _personRepository.InsertAsync(new Person(personId, "Adam", 42)); + await _unitOfWorkManager.Current.RollbackAsync(); + }); + + var person = await _personRepository.FindAsync(personId); + person.ShouldBeNull(); + } + } +} diff --git a/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs b/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs new file mode 100644 index 0000000000..77373a84d5 --- /dev/null +++ b/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.TestApp.Domain; + +namespace Volo.Abp.TestApp.EntityFrameworkCore +{ + public class TestAppDbContext : AbpDbContext + { + public DbSet People { get; set; } + + public TestAppDbContext(DbContextOptions options) + : base(options) + { + + } + } +} diff --git a/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/AbpIdentityApplicationTestModule.cs b/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/AbpIdentityApplicationTestModule.cs index 6c3e5cd2e8..d560cd69b4 100644 --- a/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/AbpIdentityApplicationTestModule.cs +++ b/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/AbpIdentityApplicationTestModule.cs @@ -5,6 +5,7 @@ using Volo.Abp.Autofac; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.Identity.EntityFrameworkCore; using Volo.Abp.Modularity; +using Volo.Abp.Uow; namespace Volo.Abp.Identity { @@ -26,6 +27,11 @@ namespace Volo.Abp.Identity context.DbContextOptions.UseInMemoryDatabase(databaseName); }); }); + + services.Configure(options => + { + options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled; //EF in-memory database does not support transactions + }); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/test/Volo.Abp.Tests/Volo/Abp/Uow/UnitOfWork_Ambient_Scope_Tests.cs b/test/Volo.Abp.Tests/Volo/Abp/Uow/UnitOfWork_Ambient_Scope_Tests.cs new file mode 100644 index 0000000000..3aa6e15b81 --- /dev/null +++ b/test/Volo.Abp.Tests/Volo/Abp/Uow/UnitOfWork_Ambient_Scope_Tests.cs @@ -0,0 +1,76 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Modularity; +using Volo.Abp.TestBase; +using Xunit; + +namespace Volo.Abp.Uow +{ + public class UnitOfWork_Ambient_Scope_Tests : AbpIntegratedTest + { + private readonly IUnitOfWorkManager _unitOfWorkManager; + + public UnitOfWork_Ambient_Scope_Tests() + { + _unitOfWorkManager = ServiceProvider.GetRequiredService(); + } + + [Fact] + public async Task UnitOfWorkManager_Current_Should_Set_Correctly() + { + _unitOfWorkManager.Current.ShouldBeNull(); + + using (var uow1 = _unitOfWorkManager.Begin()) + { + _unitOfWorkManager.Current.ShouldNotBeNull(); + _unitOfWorkManager.Current.ShouldBe(uow1); + + using (var uow2 = _unitOfWorkManager.Begin()) + { + _unitOfWorkManager.Current.ShouldNotBeNull(); + _unitOfWorkManager.Current.Id.ShouldBe(uow1.Id); + + await uow2.CompleteAsync(); + } + + _unitOfWorkManager.Current.ShouldNotBeNull(); + _unitOfWorkManager.Current.ShouldBe(uow1); + + await uow1.CompleteAsync(); + } + + _unitOfWorkManager.Current.ShouldBeNull(); + } + + [Fact] + public async Task UnitOfWorkManager_Reservation_Test() + { + _unitOfWorkManager.Current.ShouldBeNull(); + + using (var uow1 = _unitOfWorkManager.Reserve("Reservation1")) + { + _unitOfWorkManager.Current.ShouldBeNull(); + + using (var uow2 = _unitOfWorkManager.Begin()) + { + _unitOfWorkManager.Current.ShouldNotBeNull(); + _unitOfWorkManager.Current.Id.ShouldNotBe(uow1.Id); + + await uow2.CompleteAsync(); + } + + _unitOfWorkManager.Current.ShouldBeNull(); + + _unitOfWorkManager.BeginReserved("Reservation1"); + + _unitOfWorkManager.Current.ShouldNotBeNull(); + _unitOfWorkManager.Current.Id.ShouldBe(uow1.Id); + + await uow1.CompleteAsync(); + } + + _unitOfWorkManager.Current.ShouldBeNull(); + } + } +} diff --git a/test/Volo.Abp.Tests/Volo/Abp/Uow/UnitOfWork_Events_Tests.cs b/test/Volo.Abp.Tests/Volo/Abp/Uow/UnitOfWork_Events_Tests.cs index 04c061d284..a9b18b9978 100644 --- a/test/Volo.Abp.Tests/Volo/Abp/Uow/UnitOfWork_Events_Tests.cs +++ b/test/Volo.Abp.Tests/Volo/Abp/Uow/UnitOfWork_Events_Tests.cs @@ -108,5 +108,36 @@ namespace Volo.Abp.Uow failed.ShouldBeTrue(); disposed.ShouldBeTrue(); } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public void Should_Trigger_Failed_If_Rolled_Back(bool callComplete) + { + var completed = false; + var failed = false; + var disposed = false; + + Assert.Throws(() => + { + using (var uow = _unitOfWorkManager.Begin()) + { + uow.Completed += (sender, args) => completed = true; + uow.Failed += (sender, args) => { failed = true; args.IsRolledback.ShouldBeTrue(); }; + uow.Disposed += (sender, args) => disposed = true; + + uow.Rollback(); + + if (callComplete) + { + uow.Complete(); + } + } + }).Message.ShouldBe("test exception"); + + completed.ShouldBeFalse(); + failed.ShouldBeTrue(); + disposed.ShouldBeTrue(); + } } } diff --git a/test/Volo.Abp.Tests/Volo/Abp/Uow/UnitOfWork_Nested_Tests.cs b/test/Volo.Abp.Tests/Volo/Abp/Uow/UnitOfWork_Nested_Tests.cs new file mode 100644 index 0000000000..e3f2e9a0c0 --- /dev/null +++ b/test/Volo.Abp.Tests/Volo/Abp/Uow/UnitOfWork_Nested_Tests.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Modularity; +using Volo.Abp.TestBase; +using Xunit; + +namespace Volo.Abp.Uow +{ + public class UnitOfWork_Nested_Tests : AbpIntegratedTest + { + private readonly IUnitOfWorkManager _unitOfWorkManager; + + public UnitOfWork_Nested_Tests() + { + _unitOfWorkManager = ServiceProvider.GetRequiredService(); + } + + [Fact] + public async Task Should_Create_Nested_UnitOfWorks() + { + _unitOfWorkManager.Current.ShouldBeNull(); + + using (var uow1 = _unitOfWorkManager.Begin()) + { + _unitOfWorkManager.Current.ShouldNotBeNull(); + _unitOfWorkManager.Current.ShouldBe(uow1); + + using (var uow2 = _unitOfWorkManager.Begin(requiresNew: true)) + { + _unitOfWorkManager.Current.ShouldNotBeNull(); + _unitOfWorkManager.Current.Id.ShouldNotBe(uow1.Id); + + await uow2.CompleteAsync(); + } + + _unitOfWorkManager.Current.ShouldNotBeNull(); + _unitOfWorkManager.Current.ShouldBe(uow1); + + await uow1.CompleteAsync(); + } + + _unitOfWorkManager.Current.ShouldBeNull(); + } + } +}