diff --git a/docs/zh-Hans/Tutorials/Part-7.md b/docs/zh-Hans/Tutorials/Part-7.md index 602306c484..95b016e451 100644 --- a/docs/zh-Hans/Tutorials/Part-7.md +++ b/docs/zh-Hans/Tutorials/Part-7.md @@ -1 +1,238 @@ -TODO.. \ No newline at end of file +# Web应用程序开发教程 - 第七章: 数据库集成 +````json +//[doc-params] +{ + "UI": ["MVC","Blazor","BlazorServer","NG"], + "DB": ["EF","Mongo"] +} +```` +## 关于本教程 + +在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: + +* **{{DB_Text}}** 做为ORM提供程序. +* **{{UI_Value}}** 做为UI框架. + +本教程分为以下部分: + +- [Part 1: 创建服务端](Part-1.md) +- [Part 2: 图书列表页面](Part-2.md) +- [Part 3: 创建,更新和删除图书](Part-2.md) +- [Part 4: 集成测试](Part-4.md) +- [Part 5: 授权](Part-5.md) +- [Part 6: 作者: 领域层](Part-6.md) +- **Part 7: 数据库集成**(本章) +- [Part 8: 作者: 应用服务层](Part-8.md) +- [Part 9: 作者: 用户页面](Part-9.md) +- [Part 10: 图书到作者的关系](Part-10.md) + +## 下载源码 + +本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合: + +* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) +* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) + +> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). + +> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path +> `git config --system core.longpaths true` + +## 简介 + +这章阐述如何为前一章介绍的 `作者` 实体配置数据库集成. + +{{if DB=="EF"}} + +## DB Context + +打开 `Acme.BookStore.EntityFrameworkCore` 项目中的 `BookStoreDbContext` 加入 `DbSet` 属性: + +````csharp +public DbSet Authors { get; set; } +```` + +定位到相同项目中的 `BookStoreDbContext` 类中的 `OnModelCreating` 方法, 加入以下代码到方法的结尾: + +````csharp +builder.Entity(b => +{ + b.ToTable(BookStoreConsts.DbTablePrefix + "Authors", + BookStoreConsts.DbSchema); + + b.ConfigureByConvention(); + + b.Property(x => x.Name) + .IsRequired() + .HasMaxLength(AuthorConsts.MaxNameLength); + + b.HasIndex(x => x.Name); +}); +```` + +这和前面的 `Book` 实体做的一样, 所以不再赘述. + +## 创建数据库迁移 + +配置启动解决方案为使用 [Entity Framework Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/). 因为我们还没有修改数据库映射配置,所以需要创建一个新的迁移并对数据库应用变更. + +打开命令行终端, 切换当前目录为 `Acme.BookStore.EntityFrameworkCore` 项目目录, 输入以下命令: + +````bash +dotnet ef migrations add Added_Authors +```` + +这会在项目中添加一个迁移类: + +![bookstore-efcore-migration-authors](./images/bookstore-efcore-migration-authors.png) + +你可以在同一个命令行终端中使用以下命令对数据库应用更改: + +````bash +dotnet ef database update +```` + +> 如果你使用 Visual Studio, 可能希望在 *Package Manager Console (PMC)* 使用 `Add-Migration Added_Authors -c BookStoreMigrationsDbContext` 和 `Update-Database -c BookStoreMigrationsDbContext` 命令. 如果这样, 保证 {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} 是启动项目并且在PMC中 `Acme.BookStore.EntityFrameworkCore` 是 *默认项目* . + +{{else if DB=="Mongo"}} + +## DB Context + +打开 `Acme.BookStore.MongoDB` 项目 `MongoDb 文件夹`中的 `BookStoreMongoDbContext`, 在类中加入以下属性: + +````csharp +public IMongoCollection Authors => Collection(); +```` + +{{end}} + +## 实现 IAuthorRepository + +{{if DB=="EF"}} + +在 `Acme.BookStore.EntityFrameworkCore` 项目 (`Authors` 文件夹)中创建一个新类 `EfCoreAuthorRepository`, 粘贴以下代码: + +````csharp +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Threading.Tasks; +using Acme.BookStore.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace Acme.BookStore.Authors +{ + public class EfCoreAuthorRepository + : EfCoreRepository, + IAuthorRepository + { + public EfCoreAuthorRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task FindByNameAsync(string name) + { + var dbSet = await GetDbSetAsync(); + return await dbSet.FirstOrDefaultAsync(author => author.Name == name); + } + + public async Task> GetListAsync( + int skipCount, + int maxResultCount, + string sorting, + string filter = null) + { + var dbSet = await GetDbSetAsync(); + return await dbSet + .WhereIf( + !filter.IsNullOrWhiteSpace(), + author => author.Name.Contains(filter) + ) + .OrderBy(sorting) + .Skip(skipCount) + .Take(maxResultCount) + .ToListAsync(); + } + } +} +```` + +* 继承自 `EfCoreRepository`, 所以继承了标准repository的方法实现. +* `WhereIf` 是ABP 框架的快捷扩展方法. 它仅当第一个条件满足时, 执行 `Where` 查询. (根据名字查询, 仅当 filter 不为空). 你可以不使用这个方法, 但这些快捷方法可以提高效率. +* `sorting` 可以是一个字符串, 如 `Name`, `Name ASC` 或 `Name DESC`. 通过使用 [System.Linq.Dynamic.Core](https://www.nuget.org/packages/System.Linq.Dynamic.Core) NuGet 包是可能的. + +> 参阅 [EF Core 集成文档](../Entity-Framework-Core.md) 获得基于EF Core的repositories的更多信息. + +{{else if DB=="Mongo"}} + +在 `Acme.BookStore.MongoDB` 项目 (`Authors` 文件夹)中创建一个新类 `MongoDbAuthorRepository`, 粘贴以下代码: + +```csharp +using System; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Collections.Generic; +using System.Threading.Tasks; +using Acme.BookStore.MongoDB; +using MongoDB.Driver; +using MongoDB.Driver.Linq; +using Volo.Abp.Domain.Repositories.MongoDB; +using Volo.Abp.MongoDB; + +namespace Acme.BookStore.Authors +{ + public class MongoDbAuthorRepository + : MongoDbRepository, + IAuthorRepository + { + public MongoDbAuthorRepository( + IMongoDbContextProvider dbContextProvider + ) : base(dbContextProvider) + { + } + + public async Task FindByNameAsync(string name) + { + var queryable = await GetMongoQueryableAsync(); + return await queryable.FirstOrDefaultAsync(author => author.Name == name); + } + + public async Task> GetListAsync( + int skipCount, + int maxResultCount, + string sorting, + string filter = null) + { + var queryable = await GetMongoQueryableAsync(); + return await queryable + .WhereIf>( + !filter.IsNullOrWhiteSpace(), + author => author.Name.Contains(filter) + ) + .OrderBy(sorting) + .As>() + .Skip(skipCount) + .Take(maxResultCount) + .ToListAsync(); + } + } +} +``` + +* 继承自 `MongoDbRepository`, 所以继承了标准repository的方法实现. +* `WhereIf` 是ABP 框架的快捷扩展方法. 它仅当第一个条件满足时, 执行 `Where` 查询. (根据名字查询, 仅当 filter 不为空). 你可以不使用这个方法, 但这些快捷方法可以提高效率. +* `sorting` 可以是一个字符串, 如 `Name`, `Name ASC` 或 `Name DESC`. 通过使用 [System.Linq.Dynamic.Core](https://www.nuget.org/packages/System.Linq.Dynamic.Core) NuGet 包是可能的. + +> 参阅 [MongoDB 集成文档](../MongoDB.md) 获得基于MongoDB的repositories的更多信息. + +{{end}} + +## 下一章 + +查看本教程的[下一章](Part-8.md). diff --git a/docs/zh-Hans/Tutorials/images/bookstore-efcore-migration-authors.png b/docs/zh-Hans/Tutorials/images/bookstore-efcore-migration-authors.png new file mode 100644 index 0000000000..2cfb29f86f Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-efcore-migration-authors.png differ diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/AbpBackgroundWorkersHangfireModule.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/AbpBackgroundWorkersHangfireModule.cs index 19bc14a054..a17f2bd6a7 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/AbpBackgroundWorkersHangfireModule.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/AbpBackgroundWorkersHangfireModule.cs @@ -10,7 +10,7 @@ namespace Volo.Abp.BackgroundWorkers.Hangfire; [DependsOn( typeof(AbpBackgroundWorkersModule), typeof(AbpHangfireModule))] -public class AbpBackgroundWorkerHangfireModule : AbpModule +public class AbpBackgroundWorkersHangfireModule : AbpModule { public override void OnPreApplicationInitialization(ApplicationInitializationContext context) { diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs index ab1cd461e7..33e0ebde4c 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs @@ -21,7 +21,7 @@ public class HangfireBackgroundWorkerManager : IBackgroundWorkerManager, ISingle return Task.CompletedTask; } - public async Task AddAsync(IBackgroundWorker worker) + public Task AddAsync(IBackgroundWorker worker) { if (worker is IHangfireBackgroundWorker hangfireBackgroundWorker) { @@ -42,18 +42,26 @@ public class HangfireBackgroundWorkerManager : IBackgroundWorkerManager, ISingle if (worker is AsyncPeriodicBackgroundWorkerBase or PeriodicBackgroundWorkerBase) { - var timer = (AbpAsyncTimer) worker.GetType() + var timer = worker.GetType() .GetProperty("Timer", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(worker); - period = timer?.Period; + + if (worker is AsyncPeriodicBackgroundWorkerBase) + { + period = ((AbpAsyncTimer)timer)?.Period; + } + else + { + period = ((AbpTimer)timer)?.Period; + } } else { - return; + return Task.CompletedTask; } if (period == null) { - return; + return Task.CompletedTask; } var adapterType = typeof(HangfirePeriodicBackgroundWorkerAdapter<>).MakeGenericType(worker.GetType()); @@ -61,6 +69,8 @@ public class HangfireBackgroundWorkerManager : IBackgroundWorkerManager, ISingle RecurringJob.AddOrUpdate(() => workerAdapter.DoWorkAsync(), GetCron(period.Value)); } + + return Task.CompletedTask; } protected virtual string GetCron(int period) @@ -82,7 +92,7 @@ public class HangfireBackgroundWorkerManager : IBackgroundWorkerManager, ISingle } else { - cron = $"0 0 */{time.TotalDays} * *"; + throw new AbpException($"Cannot convert period: {period} to cron expression, use HangfireBackgroundWorkerBase to define worker"); } return cron; diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzPeriodicBackgroundWorkerAdapter.cs b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzPeriodicBackgroundWorkerAdapter.cs index 7c32dba467..7361fdfeb9 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzPeriodicBackgroundWorkerAdapter.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzPeriodicBackgroundWorkerAdapter.cs @@ -28,15 +28,23 @@ public class QuartzPeriodicBackgroundWorkerAdapter : QuartzBackgroundWo int? period; var workerType = worker.GetType(); - if (worker is AsyncPeriodicBackgroundWorkerBase || worker is PeriodicBackgroundWorkerBase) + if (worker is AsyncPeriodicBackgroundWorkerBase or PeriodicBackgroundWorkerBase) { if (typeof(TWorker) != worker.GetType()) { throw new ArgumentException($"{nameof(worker)} type is different from the generic type"); } - var timer = (AbpAsyncTimer)worker.GetType().GetProperty("Timer", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(worker); - period = timer?.Period; + var timer = worker.GetType().GetProperty("Timer", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(worker); + + if (worker is AsyncPeriodicBackgroundWorkerBase) + { + period = ((AbpAsyncTimer)timer)?.Period; + } + else + { + period = ((AbpTimer)timer)?.Period; + } } else { @@ -60,26 +68,26 @@ public class QuartzPeriodicBackgroundWorkerAdapter : QuartzBackgroundWo public override async Task Execute(IJobExecutionContext context) { - var worker = (IBackgroundWorker)ServiceProvider.GetService(typeof(TWorker)); + var worker = (IBackgroundWorker) ServiceProvider.GetService(typeof(TWorker)); var workerContext = new PeriodicBackgroundWorkerContext(ServiceProvider); switch (worker) { case AsyncPeriodicBackgroundWorkerBase asyncWorker: + { + if (_doWorkAsyncMethod != null) { - if (_doWorkAsyncMethod != null) - { - await (Task)_doWorkAsyncMethod.Invoke(asyncWorker, new object[] { workerContext }); - } - - break; + await (Task) _doWorkAsyncMethod.Invoke(asyncWorker, new object[] {workerContext}); } + + break; + } case PeriodicBackgroundWorkerBase syncWorker: - { - _doWorkMethod?.Invoke(syncWorker, new object[] { workerContext }); + { + _doWorkMethod?.Invoke(syncWorker, new object[] {workerContext}); - break; - } + break; + } } } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/ModuleProjectBuildPipelineBuilder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/ModuleProjectBuildPipelineBuilder.cs index 580ef7f804..35bcb71d53 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/ModuleProjectBuildPipelineBuilder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/ModuleProjectBuildPipelineBuilder.cs @@ -12,6 +12,7 @@ public static class ModuleProjectBuildPipelineBuilder pipeline.Steps.Add(new FileEntryListReadStep()); pipeline.Steps.Add(new ProjectReferenceReplaceStep()); pipeline.Steps.Add(new ReplaceCommonPropsStep()); + pipeline.Steps.Add(new MakeProxyJsonFileEmbeddedStep()); pipeline.Steps.Add(new ReplaceConfigureAwaitPropsStep()); pipeline.Steps.Add(new UpdateNuGetConfigStep("/NuGet.Config")); pipeline.Steps.Add(new CreateProjectResultZipStep()); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/MakeProxyJsonFileEmbeddedStep.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/MakeProxyJsonFileEmbeddedStep.cs new file mode 100644 index 0000000000..f8de452172 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/MakeProxyJsonFileEmbeddedStep.cs @@ -0,0 +1,38 @@ +using System.Linq; +using System.Xml.Linq; +using Volo.Abp.Cli.Utils; + +namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps; + +public class MakeProxyJsonFileEmbeddedStep : ProjectBuildPipelineStep +{ + public override void Execute(ProjectBuildContext context) + { + foreach (var file in context.Files.Where(x => x.Name.EndsWith(".HttpApi.Client.csproj"))) + { + using (var stream = StreamHelper.GenerateStreamFromString(file.Content)) + { + var doc = XDocument.Load(stream); + + if (doc.Root == null) + { + continue; + } + + var itemGroupNode = + new XElement("ItemGroup", + new XElement("EmbeddedResource", + new XAttribute("Include", @"**\*generate-proxy.json") + ), + new XElement("Content", + new XAttribute("Remove", @"**\*generate-proxy.json") + ) + ); + + doc.Root.Add(itemGroupNode); + + file.SetContent(doc.ToString()); + } + } + } +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Options/PreConfigureActionList.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Options/PreConfigureActionList.cs index 55d16b34dd..bf8bb90f55 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Options/PreConfigureActionList.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Options/PreConfigureActionList.cs @@ -12,4 +12,11 @@ public class PreConfigureActionList : List> action(options); } } + + public TOptions Configure() + { + var options = Activator.CreateInstance(); + Configure(options); + return options; + } } diff --git a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/AbpEventBusRebusModule.cs b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/AbpEventBusRebusModule.cs index db819093a6..657c34d2be 100644 --- a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/AbpEventBusRebusModule.cs +++ b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/AbpEventBusRebusModule.cs @@ -11,18 +11,17 @@ public class AbpEventBusRebusModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - var options = context.Services.ExecutePreConfiguredActions(); ; - context.Services.AddTransient(typeof(IHandleMessages<>), typeof(RebusDistributedEventHandlerAdapter<>)); + var preActions = context.Services.GetPreConfigureActions(); Configure(rebusOptions => { - context.Services.ExecutePreConfiguredActions(rebusOptions); + preActions.Configure(rebusOptions); }); context.Services.AddRebus(configure => { - options.Configurer?.Invoke(configure); + preActions.Configure().Configurer?.Invoke(configure); return configure; }); } @@ -36,4 +35,4 @@ public class AbpEventBusRebusModule : AbpModule .GetRequiredService() .Initialize(); } -} +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs index f12c3e32d9..1eb7689aba 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Reflection; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -6,6 +6,7 @@ using Microsoft.Extensions.Options; using Volo.Abp.Collections; using Volo.Abp.DependencyInjection; using Volo.Abp.EventBus.Local; +using Volo.Abp.Uow; namespace Volo.Abp.EventBus.Distributed; @@ -15,16 +16,19 @@ public class LocalDistributedEventBus : IDistributedEventBus, ISingletonDependen { private readonly ILocalEventBus _localEventBus; + protected IUnitOfWorkManager UnitOfWorkManager { get; } protected IServiceScopeFactory ServiceScopeFactory { get; } protected AbpDistributedEventBusOptions AbpDistributedEventBusOptions { get; } public LocalDistributedEventBus( ILocalEventBus localEventBus, + IUnitOfWorkManager unitOfWorkManager, IServiceScopeFactory serviceScopeFactory, IOptions distributedEventBusOptions) { _localEventBus = localEventBus; + UnitOfWorkManager = unitOfWorkManager; ServiceScopeFactory = serviceScopeFactory; AbpDistributedEventBusOptions = distributedEventBusOptions.Value; Subscribe(distributedEventBusOptions.Value.Handlers); @@ -122,24 +126,56 @@ public class LocalDistributedEventBus : IDistributedEventBus, ISingletonDependen _localEventBus.UnsubscribeAll(eventType); } - public Task PublishAsync(TEvent eventData, bool onUnitOfWorkComplete = true) + public async Task PublishAsync(TEvent eventData, bool onUnitOfWorkComplete = true) where TEvent : class { - return _localEventBus.PublishAsync(eventData, onUnitOfWorkComplete); + await PublishAsync(typeof(TEvent), eventData, onUnitOfWorkComplete); } - public Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true) + public async Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true) { - return _localEventBus.PublishAsync(eventType, eventData, onUnitOfWorkComplete); + if (onUnitOfWorkComplete && UnitOfWorkManager.Current != null) + { + AddToUnitOfWork( + UnitOfWorkManager.Current, + new UnitOfWorkEventRecord(eventType, eventData, EventOrderGenerator.GetNext()) + ); + return; + } + + await _localEventBus.PublishAsync(eventType, eventData, onUnitOfWorkComplete: false); + } + + public async Task PublishAsync(TEvent eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) + where TEvent : class + { + await PublishAsync(typeof(TEvent), eventData, onUnitOfWorkComplete, useOutbox); } - public Task PublishAsync(TEvent eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) where TEvent : class + public async Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) { - return _localEventBus.PublishAsync(eventData, onUnitOfWorkComplete); + if (onUnitOfWorkComplete && UnitOfWorkManager.Current != null) + { + AddToUnitOfWork( + UnitOfWorkManager.Current, + new UnitOfWorkEventRecord(eventType, eventData, EventOrderGenerator.GetNext(), useOutbox) + ); + return; + } + + if (useOutbox && UnitOfWorkManager.Current != null) + { + UnitOfWorkManager.Current.OnCompleted(async() => { + await _localEventBus.PublishAsync(eventType, eventData, onUnitOfWorkComplete: false); + }); + return; + } + + await _localEventBus.PublishAsync(eventType, eventData, onUnitOfWorkComplete: false); } - public Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) + protected virtual void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord) { - return _localEventBus.PublishAsync(eventType, eventData, onUnitOfWorkComplete); + unitOfWork.AddOrReplaceDistributedEvent(eventRecord); } } diff --git a/framework/src/Volo.Abp.HangFire/Volo/Abp/Hangfire/AbpHangfireModule.cs b/framework/src/Volo.Abp.HangFire/Volo/Abp/Hangfire/AbpHangfireModule.cs index b40871e82a..b6de8013b1 100644 --- a/framework/src/Volo.Abp.HangFire/Volo/Abp/Hangfire/AbpHangfireModule.cs +++ b/framework/src/Volo.Abp.HangFire/Volo/Abp/Hangfire/AbpHangfireModule.cs @@ -13,9 +13,10 @@ public class AbpHangfireModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { + var preActions = context.Services.GetPreConfigureActions(); context.Services.AddHangfire(configuration => { - context.Services.ExecutePreConfiguredActions(configuration); + preActions.Configure(configuration); }); } diff --git a/framework/src/Volo.Abp.Http.Client.Web/Volo/Abp/Http/Client/Web/Conventions/AbpHttpClientProxyServiceConvention.cs b/framework/src/Volo.Abp.Http.Client.Web/Volo/Abp/Http/Client/Web/Conventions/AbpHttpClientProxyServiceConvention.cs index 3a0ef18d49..cd5f98b6d6 100644 --- a/framework/src/Volo.Abp.Http.Client.Web/Volo/Abp/Http/Client/Web/Conventions/AbpHttpClientProxyServiceConvention.cs +++ b/framework/src/Volo.Abp.Http.Client.Web/Volo/Abp/Http/Client/Web/Conventions/AbpHttpClientProxyServiceConvention.cs @@ -21,6 +21,7 @@ namespace Volo.Abp.Http.Client.Web.Conventions; public class AbpHttpClientProxyServiceConvention : AbpServiceConvention { protected readonly IClientProxyApiDescriptionFinder ClientProxyApiDescriptionFinder; + protected readonly List ControllerWithAttributeRoute; protected readonly List ActionWithAttributeRoute; public AbpHttpClientProxyServiceConvention( @@ -30,6 +31,7 @@ public class AbpHttpClientProxyServiceConvention : AbpServiceConvention : base(options, conventionalRouteBuilder) { ClientProxyApiDescriptionFinder = clientProxyApiDescriptionFinder; + ControllerWithAttributeRoute = new List(); ActionWithAttributeRoute = new List(); } @@ -51,15 +53,6 @@ public class AbpHttpClientProxyServiceConvention : AbpServiceConvention { controller.ControllerName = controller.ControllerName.RemovePostFix("ClientProxy"); - var moduleApiDescription = FindModuleApiDescriptionModel(controller); - - if (moduleApiDescription != null && !moduleApiDescription.RootPath.IsNullOrWhiteSpace()) - { - var selector = controller.Selectors.FirstOrDefault(); - selector?.EndpointMetadata.Add(new AreaAttribute(moduleApiDescription.RootPath)); - controller.RouteValues.Add(new KeyValuePair("area", moduleApiDescription.RootPath)); - } - var controllerApiDescription = FindControllerApiDescriptionModel(controller); if (controllerApiDescription != null && !controllerApiDescription.ControllerGroupName.IsNullOrWhiteSpace()) @@ -77,6 +70,14 @@ public class AbpHttpClientProxyServiceConvention : AbpServiceConvention { RemoveEmptySelectors(controller.Selectors); + var moduleApiDescription = FindModuleApiDescriptionModel(controller); + if (moduleApiDescription != null && !moduleApiDescription.RootPath.IsNullOrWhiteSpace()) + { + var selector = controller.Selectors.FirstOrDefault(); + selector?.EndpointMetadata.Add(new AreaAttribute(moduleApiDescription.RootPath)); + controller.RouteValues.Add(new KeyValuePair("area", moduleApiDescription.RootPath)); + } + var controllerType = controller.ControllerType.AsType(); var remoteServiceAtt = ReflectionHelper.GetSingleAttributeOrDefault(controllerType.GetTypeInfo()); if (remoteServiceAtt != null && !remoteServiceAtt.IsEnabledFor(controllerType)) @@ -111,15 +112,14 @@ public class AbpHttpClientProxyServiceConvention : AbpServiceConvention return;; } + ControllerWithAttributeRoute.Add(controller); ActionWithAttributeRoute.Add(action); if (!action.Selectors.Any()) { var abpServiceSelectorModel = new SelectorModel { - AttributeRouteModel = new AttributeRouteModel( - new RouteAttribute(template: actionApiDescriptionModel.Url) - ), + AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(template: actionApiDescriptionModel.Url)), ActionConstraints = { new HttpMethodActionConstraint(new[] { actionApiDescriptionModel.HttpMethod }) } }; @@ -142,9 +142,7 @@ public class AbpHttpClientProxyServiceConvention : AbpServiceConvention if (selector.AttributeRouteModel == null) { - selector.AttributeRouteModel = new AttributeRouteModel( - new RouteAttribute(template: actionApiDescriptionModel.Url) - ); + selector.AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(template: actionApiDescriptionModel.Url)); } if (!selector.ActionConstraints.OfType().Any()) @@ -157,14 +155,17 @@ public class AbpHttpClientProxyServiceConvention : AbpServiceConvention protected virtual void ConfigureClientProxyApiExplorer(ControllerModel controller) { - if (controller.ApiExplorer.GroupName.IsNullOrEmpty()) + if (ControllerWithAttributeRoute.Contains(controller)) { - controller.ApiExplorer.GroupName = controller.ControllerName; - } + if (controller.ApiExplorer.GroupName.IsNullOrEmpty()) + { + controller.ApiExplorer.GroupName = controller.ControllerName; + } - if (controller.ApiExplorer.IsVisible == null) - { - controller.ApiExplorer.IsVisible = IsVisibleRemoteService(controller.ControllerType); + if (controller.ApiExplorer.IsVisible == null) + { + controller.ApiExplorer.IsVisible = IsVisibleRemoteService(controller.ControllerType); + } } foreach (var action in controller.Actions) diff --git a/framework/src/Volo.Abp.Json/Volo/Abp/Json/AbpJsonModule.cs b/framework/src/Volo.Abp.Json/Volo/Abp/Json/AbpJsonModule.cs index d797b9dfa4..77f791cd27 100644 --- a/framework/src/Volo.Abp.Json/Volo/Abp/Json/AbpJsonModule.cs +++ b/framework/src/Volo.Abp.Json/Volo/Abp/Json/AbpJsonModule.cs @@ -16,12 +16,11 @@ public class AbpJsonModule : AbpModule context.Services.TryAddEnumerable(ServiceDescriptor .Transient, AbpSystemTextJsonSerializerOptionsSetup>()); + var preActions = context.Services.GetPreConfigureActions(); Configure(options => { options.Providers.Add(); - - var abpJsonOptions = context.Services.ExecutePreConfiguredActions(); - if (abpJsonOptions.UseHybridSerializer) + if (preActions.Configure().UseHybridSerializer) { options.Providers.Add(); } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Versioning.Tests/Volo/Abp/AspNetCore/Mvc/Versioning/AbpAspNetCoreMvcVersioningTestModule.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Versioning.Tests/Volo/Abp/AspNetCore/Mvc/Versioning/AbpAspNetCoreMvcVersioningTestModule.cs index 0bedc4d049..a8d2a4ff70 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Versioning.Tests/Volo/Abp/AspNetCore/Mvc/Versioning/AbpAspNetCoreMvcVersioningTestModule.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Versioning.Tests/Volo/Abp/AspNetCore/Mvc/Versioning/AbpAspNetCoreMvcVersioningTestModule.cs @@ -38,9 +38,10 @@ public class AbpAspNetCoreMvcVersioningTestModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { + var preActions = context.Services.GetPreConfigureActions(); Configure(options => { - context.Services.ExecutePreConfiguredActions(options); + preActions.Configure(options); }); context.Services.AddAbpApiVersioning(options => @@ -48,11 +49,10 @@ public class AbpAspNetCoreMvcVersioningTestModule : AbpModule options.ReportApiVersions = true; options.AssumeDefaultVersionWhenUnspecified = true; - //options.ApiVersionReader = new HeaderApiVersionReader("api-version"); //Supports header too - //options.ApiVersionReader = new MediaTypeApiVersionReader(); //Supports accept header too + //options.ApiVersionReader = new HeaderApiVersionReader("api-version"); //Supports header too + //options.ApiVersionReader = new MediaTypeApiVersionReader(); //Supports accept header too - var mvcOptions = context.Services.ExecutePreConfiguredActions(); - options.ConfigureAbp(mvcOptions); + options.ConfigureAbp(preActions.Configure()); }); context.Services.AddHttpClientProxies(typeof(AbpAspNetCoreMvcVersioningTestModule).Assembly); diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs index 8a98a6a94c..497503680d 100644 --- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs @@ -1,7 +1,9 @@ using System; using System.Threading.Tasks; +using Shouldly; using Volo.Abp.Domain.Entities.Events.Distributed; using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; using Xunit; namespace Volo.Abp.EventBus.Distributed; @@ -55,11 +57,75 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase await DistributedEventBus.PublishAsync(new MySimpleEto { Properties = - { - {"TenantId", tenantId.ToString()} - } + { + {"TenantId", tenantId.ToString()} + } }); Assert.Equal(tenantId, MySimpleDistributedSingleInstanceEventHandler.TenantId); } + + [Fact] + public async Task Event_Should_Published_On_UnitOfWorkComplete() + { + var id = 0; + DistributedEventBus.Subscribe(data => + { + id = data.Value; + return Task.CompletedTask; + }); + + var unitOfWorkManager = GetRequiredService(); + using (var uow = unitOfWorkManager.Begin()) + { + await DistributedEventBus.PublishAsync(new MySimpleEventData(3), onUnitOfWorkComplete: true, useOutbox: false); + } + id.ShouldBe(0); + + using (var uow = unitOfWorkManager.Begin()) + { + await DistributedEventBus.PublishAsync(new MySimpleEventData(3), onUnitOfWorkComplete: true, useOutbox: false); + await uow.CompleteAsync(); + } + id.ShouldBe(3); + + id = 0; + using (var uow = unitOfWorkManager.Begin()) + { + await DistributedEventBus.PublishAsync(new MySimpleEventData(3), onUnitOfWorkComplete: false, useOutbox: false); + } + id.ShouldBe(3); + } + + [Fact] + public async Task Event_Should_Published_On_UnitOfWorkComplete_UseOutbox() + { + var id = 0; + DistributedEventBus.Subscribe(data => + { + id = data.Value; + return Task.CompletedTask; + }); + + var unitOfWorkManager = GetRequiredService(); + using (var uow = unitOfWorkManager.Begin()) + { + await DistributedEventBus.PublishAsync(new MySimpleEventData(3), onUnitOfWorkComplete: false, useOutbox: true); + } + id.ShouldBe(0); + + using (var uow = unitOfWorkManager.Begin()) + { + await DistributedEventBus.PublishAsync(new MySimpleEventData(3), onUnitOfWorkComplete: false, useOutbox: true); + await uow.CompleteAsync(); + } + id.ShouldBe(3); + + id = 0; + using (var uow = unitOfWorkManager.Begin()) + { + await DistributedEventBus.PublishAsync(new MySimpleEventData(3), onUnitOfWorkComplete: false, useOutbox: false); + } + id.ShouldBe(3); + } } diff --git a/npm/ng-packs/package.json b/npm/ng-packs/package.json index eca26a6b0f..e49dbd2ce1 100644 --- a/npm/ng-packs/package.json +++ b/npm/ng-packs/package.json @@ -56,6 +56,17 @@ "@angular/platform-browser": "12.2.13", "@angular/platform-browser-dynamic": "12.2.13", "@angular/router": "12.2.13", + "@abp/ng.account": "~5.0.1", + "@abp/ng.account.core": "~5.0.1", + "@abp/ng.core": "~5.0.1", + "@abp/ng.feature-management": "~5.0.1", + "@abp/ng.identity": "~5.0.1", + "@abp/ng.permission-management": "~5.0.1", + "@abp/ng.schematics": "~5.0.1", + "@abp/ng.setting-management": "~5.0.1", + "@abp/ng.tenant-management": "~5.0.1", + "@abp/ng.theme.basic": "~5.0.1", + "@abp/ng.theme.shared": "~5.0.1", "@fortawesome/fontawesome-free": "^5.15.4", "@ng-bootstrap/ng-bootstrap": "11.0.0-beta.2", "@ngneat/spectator": "^8.0.3", diff --git a/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts index ba4fd361c2..2076be84b4 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts @@ -198,6 +198,5 @@ describe('DynamicLayoutComponent', () => { spectator.detectComponentChanges(); expect(spectator.query('abp-layout-empty')).toBeFalsy(); - expect(spectator.query('abp-dynamic-layout').children[0].tagName).toEqual('ROUTER-OUTLET'); }); });