本文主要讲解了如何把ABP官方的在线生成解决方案运行起来,并说明了解决方案中项目间的依赖关系。然后手动实践了如何从0搭建了一个简化的解决方案。ABP官方的在线生成解决方案源码下载参考[3],手动搭建的简化的解决方案源码下载参考[4]。
一.ABP官方在线生成解决方案
1.将在线生成解决方案跑起来
首先进入页面https://abp.io/get-started,然后创建项目:
然后头脑中要有一个项目之间的依赖关系图,不清楚的可以参考《基于ABP实现DDD》:
截止目前为止,项目使用的.NET版本是6.0,ABP版本是5.3.3。
使用Rider打开项目Acme.BookStore后,会提示使用yarn安装package,安装包后:
在整个解决方案中搜索ConnectionStrings,发现其在Acme.BookStore.HttpApi.Host、Acme.BookStore.DbMigrator和Acme.BookStore.IdentityServer这3个启动项目中出现:
将ConnectionStrings中的内容替换为:
"ConnectionStrings": { "Default": "Server=127.0.0.1;Database=BookStore;Trusted_Connection=True;User ID=sa;Password=913292836;" },
然后开始运行Acme.BookStore.DbMigrator进行数据种子迁移:
出现上面图片输出结果,基本上表示迁移成功完成了,接下来看看数据库:
运行Acme.BookStore.Web项目如下:
启动后发现报错了,发现主要是3个问题:一个是Redis没有启动,另一个问题是IDS4服务没有启动,最后一个问题是Acme.BookStore.HttpApi.Host没有启动:
启动IDS4服务的时候发现报错Volo.Abp.AbpException: Could not find the bundle file '/libs/abp/core/abp.css' for the bundle 'Basic.Global'!
:
在该项目下执行命令abp install-libs:
发现消息提示说ABP CLI有个更新的5.3.3版本,通过命令dotnet tool update -g Volo.Abp.Cli
进行升级。再次运行Acme.BookStore.IdentityServer项目,发现不报错误了。如果在启动其它项目(特指Acme.BookStore.Web项目)的时候报同样的错误,那么同样执行命令abp install-libs即可解决问题。同时启动这3个项目如下:
启动成功后就可以见到熟悉的界面:
下面是项目Swagger的界面:
至此,已经把从官方下载下来的项目成功地运行起来了。
2.ABP运行流程
下面是在网上[1]找到的一张图,很清晰的说明了AspNet Core和ABP模块的运行流程,个人认为图上的Startup.ConfigureServices应该是Startup.Configure,已经在图中做了修改。
(1)AspNet Core运行流程
简单理解,基本上就是在Startup.ConfigureServices中进行依赖注入配置,然后在Startup.Configure中配置管道中间件,访问的时候就像洋葱模型。
(2)ABP模块运行流程
- 在ABP模块中对Startup.ConfigureServices做了扩展,增加了PreConfigureServices和PostConfigureServices。对Startup.Configure也做了扩展,当然名字也修改了,Startup.Configure相当于是OnApplicationInitialization,同时增加了OnPreApplicationInitialization和OnPostApplicationInitialization。
- 在ABP解决方案中有多个项目,每个项目都会有一个类继承自AbpModule。并且通过DependsOn描述了该模块依赖的模块。这样在ABP解决方案中就会有很多模块之间的依赖关系,通过拓扑排序算法对模块进行排序,从最深层的模块依次加载,直到启动所有模块。
(3)AbpModule抽象类中的方法
除了主要的PreConfigureServices()、ConfigureServices()、PostConfigureServices()、OnPreApplicationInitialization()、OnApplicationInitialization()、OnPostApplicationInitialization()方法外,还有一些其它的方法。abpframeworksrcVolo.Abp.CoreVoloAbpModularityAbpModule.cs
:
二.手动创建解决方案
0.创建解决方案
首先创建一个目录BookStoreHand用于存放解决方案:
然后创建一个解决方案,执行命令dotnet new sln -n Acme.BookStore:
用Rider打开后解决方案是空的,然后手动创建2个New Solution Folder,分别为src和test:
1.创建领域共享层和领域层
(1)Acme.BookStore.Domain.Shared[领域共享层]
通常定义的常量和枚举,都放在该项目中。通过命令dotnet new classlib -n Acme.BookStore.Domain.Shared
和dotnet sln ../Acme.BookStore.sln add Acme.BookStore.Domain.Shared
创建领域共享层,并将其添加到解决方案当中:
然后就是创建模块类BookStoreDomainSharedModule如下:
namespace Acme.BookStore.Domain.Shared { public class BookStoreDomainSharedModule: AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { base.ConfigureServices(context); } public override void OnApplicationInitialization(ApplicationInitializationContext context) { base.OnApplicationInitialization(context); } } }
说明:接下来创建项目、添加项目间的引用等都使用Rider,而不再使用CLI操作,觉得CLI并不方便操作。基本思路顺序都是:创建项目,设置引用关系,创建模块,其它操作。
(2)Acme.BookStore.Domain[领域层]
该项目包含实体、值对象、领域服务、规约、仓储接口等。通过Rider创建Class Library项目Acme.BookStore.Domain如下:
Acme.BookStore.Domain项目依赖于Acme.BookStore.Domain.Shared项目:
创建模块类BookStoreDomainSharedModule如下:
[DependsOn( typeof(BookStoreDomainSharedModule) //依赖领域共享模块 )] public class BookStoreDomainModule: AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { base.ConfigureServices(context); } public override void OnApplicationInitialization(ApplicationInitializationContext context) { base.OnApplicationInitialization(context); } }
创建领域实体Book:
public class Book: Entity<int> { public string BookName { get; set; } //名字 public string Author { get; set; } //作者 public DateTime PublishDate { get; set; } //出版日期 public double Price { get; set; } //价格 }
创建仓储IBookRepository接口:
public interface IBookRepository: IRepository<Book, int> { }
2.创建基础设施层
(1)创建项目
基础设施层Acme.BookStore.EntityFrameworkCore是EF Core核心基础依赖项目,包含数据上下文、数据库映射、EF Core仓储实现等。通过Rider创建Class Library项目Acme.BookStore.EntityFrameworkCore如下:
Acme.BookStore.EntityFrameworkCore项目依赖于Acme.BookStore.Domain项目:
(2)创建模块
创建模块类BookStoreEntityFrameworkCoreModule如下:
[DependsOn( typeof(BookStoreDomainModule), typeof(AbpEntityFrameworkCoreSqlServerModule) )] public class BookStoreEntityFrameworkCoreModule: AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddAbpDbContext<BookStoreDbContext>(options => { // 给所有的实体都增加默认仓储 options.AddDefaultRepositories(includeAllEntities: true); }); Configure<AbpDbContextOptions>(options => { options.UseSqlServer(); }); } public override void OnApplicationInitialization(ApplicationInitializationContext context) { base.OnApplicationInitialization(context); } }
(3)创建数据库上下文
创建数据库上下文BookStoreDbContext:
public class BookStoreDbContext: AbpDbContext<BookStoreDbContext> { public BookStoreDbContext(DbContextOptions<BookStoreDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfigurationsFromAssembly(typeof(BookStoreDbContext).Assembly); } }
ApplyConfigurationsFromAssembly应用来自IEntityTypeConfiguration中的配置。定义实体映射BookDbMapping如下:
public class BookDbMapping: IEntityTypeConfiguration<Book> { public void Configure(EntityTypeBuilder<Book> builder) { // 配置主键 builder.HasKey(b => b.Id).HasName("Id"); // 配置表和字段 builder.ToTable("AbpBook"); builder.Property(t => t.BookName).IsRequired().HasColumnName("BookName").HasComment("书名"); builder.Property(t => t.Author).IsRequired().HasColumnName("Author").HasComment("作者"); builder.Property(t => t.PublishDate).IsRequired().HasColumnName("PublishDate").HasComment("出版日期"); builder.Property(t => t.Price).IsRequired().HasColumnName("Price").HasComment("价格"); // 配置关系 } }
(4)创建仓储实现
定义IBookRepository的实现BookRepository如下:
public class BookRepository: EfCoreRepository<BookStoreDbContext, Book, int>, IBookRepository { public BookRepository(IDbContextProvider<BookStoreDbContext> dbContextProvider) : base(dbContextProvider) { } }
3.创建应用契约层和应用层
(1)Acme.BookStore.Application.Contracts[应用契约层]
包含应用服务接口和数据传输对象。该项⽬被应⽤程序客户端引用,比如Web项目、API客户端项目。通过Rider创建Class Library项目Acme.BookStore.Application.Contracts:
Acme.BookStore.Application.Contracts项目依赖于Acme.BookStore.Domain.Shared项目如下:
创建模块类BookStoreApplicationContractsModule如下:
[DependsOn( typeof(BookStoreDomainSharedModule), //依赖于BookStoreDomainSharedModule typeof(AbpObjectExtendingModule) //依赖于AbpObjectExtendingModule )] public class BookStoreApplicationContractsModule: AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { base.ConfigureServices(context); } public override void OnApplicationInitialization(ApplicationInitializationContext context) { base.OnApplicationInitialization(context); } }
创建服务接口IBookAppService如下:
public interface IBookAppService: IApplicationService { /// <summary> /// 获取书籍 /// </summary> Task<BookDto> GetBookAsync(int id); }
创建输出DTO为BookDto如下:
public class BookDto { public int Id { get; set; } //主键 public string BookName { get; set; } //名字 public string Author { get; set; } //作者 public DateTime PublishDate { get; set; } //出版日期 public double Price { get; set; } //价格 }
(2)Acme.BookStore.Application[应用层]
实现在Contracts项目中定义的接⼝。通过Rider创建Class Library项目Acme.BookStore.Application如下:
Acme.BookStore.Application项目依赖于
Acme.BookStore.Application.Contracts和Acme.BookStore.Domain项目:
创建模块类BookStoreApplicationModule如下:
[DependsOn( typeof(AbpAutoMapperModule), //依赖于AutoMapper typeof(BookStoreDomainModule), //依赖于BookStoreDomainModule typeof(BookStoreApplicationContractsModule) //依赖于BookStoreApplicationContractsModule )] public class BookStoreApplicationModule: AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { var services = context.Services; // 添加ObjectMapper注入 services.AddAutoMapperObjectMapper<BookStoreApplicationModule>(); // Abp AutoMapper设置 Configure<AbpAutoMapperOptions>(config => { config.AddMaps<BookStoreApplicationAutoMapperProfile>(); }); } public override void OnApplicationInitialization(ApplicationInitializationContext context) { base.OnApplicationInitialization(context); } }
创建自动映类BookStoreApplicationAutoMapperProfile如下:
public class BookStoreApplicationAutoMapperProfile: Profile { public BookStoreApplicationAutoMapperProfile() { CreateMap<BookDto, Book>(); CreateMap<Book, BookDto>(); } }
创建IBookAppService类的实现类BookAppService如下:
public class BookAppService: ApplicationService, IBookAppService { private readonly IBookRepository _bookRepository; public BookAppService(IBookRepository bookRepository) { _bookRepository = bookRepository; } public async Task<BookDto> GetBookAsync(int id) { var queryable = await _bookRepository.GetQueryableAsync(); var book = queryable.FirstOrDefault(t => t.Id == id); if (book == null) { throw new ArgumentNullException(nameof(book)); } return ObjectMapper.Map<Book, BookDto>(book); } }
4.创建种子迁移
Acme.BookStore.DbMigrator是控制台应用程序,主要是迁移数据库结构并初始化种子数据。通过Rider创建ASP.NET Core Web Application的Empty项目Acme.BookStore.DbMigrator如下:
Acme.BookStore.DbMigrator项目依赖于Acme.BookStore.Application.Contracts和Acme.BookStore.EntityFrameworkCore项目如下:
创建模块类BookStoreDbMigratorModule如下:
[DependsOn( typeof(AbpAutofacModule), typeof(BookStoreEntityFrameworkCoreModule), typeof(BookStoreApplicationContractsModule) )] public class BookStoreDbMigratorModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { base.ConfigureServices(context); } public override void OnApplicationInitialization(ApplicationInitializationContext context) { base.OnApplicationInitialization(context); } }
在Program.cs中添加services.AddHostedService
说明:Program.cs、DbMigratorHostedService.cs和BookStoreDbMigrationService.cs的源码等完整项目源码参考[4]。
5.创建远程服务层
Acme.BookStore.HttpApi[远程服务层],简单理解就是很薄的控制层,该项目主要用于定义HTTP API,即应用服务层的包装器,将它们公开给远程客户端调用。通过Rider创建Class Library项目Acme.BookStore.HttpApi如下:
Acme.BookStore.HttpApi项目依赖于Acme.BookStore.Application.Contracts项目如下:
创建模块类BookStoreHttpApiModule如下:
[DependsOn( typeof(BookStoreApplicationContractsModule) //依赖于BookStoreApplicationContractsModule )] public class BookStoreHttpApiModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { base.ConfigureServices(context); } public override void OnApplicationInitialization(ApplicationInitializationContext context) { base.OnApplicationInitialization(context); } }
为了简要说明问题,创建一个简单的控制器类BookStoreController如下:
[RemoteService] [Area("BookStore")] [Route("api/app/book")] public class BookStoreController: AbpControllerBase { private readonly IBookAppService _bookAppService; public BookStoreController(IBookAppService bookAppService) { _bookAppService = bookAppService; } [HttpGet] [Route("get-book")] public Task<BookDto> GetBookAsync(int id) { return _bookAppService.GetBookAsync(id); } }
6.创建展示层
Acme.BookStore.HttpApi.Host这个是前后端分离时的项目命名方式。通过Rider创建ASP.NET Core Web Application的Empty项目Acme.BookStore.HttpApi.Host如下:
Acme.BookStore.HttpApi.Host项目依赖于Acme.BookStore.Application、Acme.BookStore.EntityFrameworkCore和Acme.BookStore.HttpApi项目如下:
创建模块类BookStoreHttpApiHostModule如下:
[DependsOn( typeof(BookStoreHttpApiModule), //依赖于BookStoreHttpApiModule typeof(AbpAutofacModule), //依赖于AbpAutofacModule typeof(BookStoreApplicationModule), //依赖于BookStoreApplicationModule typeof(BookStoreEntityFrameworkCoreModule), //依赖于BookStoreEntityFrameworkCoreModule typeof(AbpAspNetCoreSerilogModule), //依赖于AbpAspNetCoreSerilogModule typeof(AbpSwashbuckleModule) //依赖于AbpSwashbuckleModule )] public class BookStoreHttpApiHostModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { var services = context.Services; var configuration = services.GetConfiguration(); ConfigureConventionalControllers(); ConfigureCors(context, configuration); ConfigureSwaggerServices(context, configuration); } private void ConfigureConventionalControllers() { Configure<AbpAspNetCoreMvcOptions>(options => { options.ConventionalControllers.Create(typeof(BookStoreApplicationModule).Assembly); }); } private void ConfigureCors(ServiceConfigurationContext context, IConfiguration configuration) { context.Services.AddCors(options => { options.AddPolicy("AllowAll",builder => { builder .WithOrigins( configuration["App:CorsOrigins"] .Split(",", StringSplitOptions.RemoveEmptyEntries) .Select(o => o.RemovePostFix("/")) .ToArray() ) .WithAbpExposedHeaders() .SetIsOriginAllowedToAllowWildcardSubdomains() .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); }); } private static void ConfigureSwaggerServices(ServiceConfigurationContext context, IConfiguration configuration) { context.Services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo { Title = "BookStore API", Version = "v1" }); options.DocInclusionPredicate((docName, description) => true); options.CustomSchemaIds(type => type.FullName); }); } public override void OnApplicationInitialization(ApplicationInitializationContext context) { var env = context.GetEnvironment(); var app = context.GetApplicationBuilder(); var configuration = context.GetConfiguration(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseCors("AllowAll"); if (configuration["UseSwagger"] == "true") { app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "Acme.BookStore API"); }); } app.UseRouting(); app.UseConfiguredEndpoints(); } }
说明:HomeController.cs、Program.cs和配置文件等项目完整源码参考[4]。
将Acme.BookStore.HttpApi.Host项目启动起来后如下:
通过Swagger界面测试https://localhost:7016/api/app/book/get-book?id=1
接口如下:
奇怪的是在线生成解决方案的时候,UI框架选择了MVC,但是还是出现了这个项目,并且在启动Acme.BookStore.Web项目的时候,如果不启动Acme.BookStore.HttpApi.Host项目,还会报错Volo.Abp.AbpException: Remote service 'AbpMvcClient' was not found and there is no default configuration
,并且还没有找到Acme.BookStore.Web项目在哪里用到了Acme.BookStore.HttpApi.Host项目。因为自己主要关注前后端分离的项目,所以就不纠结这个细节了。
在线生成解决方案中还包括:Acme.BookStore.IdentityServer(认证授权项目),Acme.BookStore.HttpApi.Client(远程服务代理层),Acme.BookStore.Web(前后端不分离的展示层),Acme.BookStore.TestBase(其它项目共享或使用的类),Acme.BookStore.Domain.Tests(测试领域层对象),Acme.BookStore.EntityFrameworkCore.Tests(测试自定义仓储实现或EF Core映射),Acme.BookStore.Application.Tests(测试应用层对象),Acme.BookStore.HttpApi.Client.ConsoleTestApp(从.NET控制台中调用HTTP API)等。一篇文章放不下,后面继续解说实践。
参考文献:
[1]聊一聊ABP vNext的模块化系统:https://www.sohu.com/a/436373048_468635
[2]Abp vNext源码分析文章目录:https://www.cnblogs.com/myzony/p/10722506.html
[3]手动从0搭建ABP框架-ABP官方完整解决方案源码:https://url39.ctfile.com/f/2501739-625678611-787336?p=2096 (访问密码: 2096)
[4]手动从0搭建ABP框架-手动搭建简化解决方案源码:https://url39.ctfile.com/f/2501739-625678627-091eb9?p=2096 (访问密码: 2096)