本文主要讲解了如何把 ABP 官方的在线生成解决方案运行起来,并说明了解决方案中项目间的依赖关系。然后手动实践了如何从 0 搭建了一个简化的解决方案。ABP 官方的在线生成解决方案源码下载参考[3],手动搭建的简化的解决方案源码下载参考[4]。
一.ABP 官方在线生成解决方案
1.将在线生成解决方案跑起来
首先进入页面 https://abp.io/get-started,然后创建项目:然后头脑中要有一个项目之间的依赖关系图,不清楚的可以参考《
截止目前为止,项目使用的.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
{
}
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()这个数据库迁移主机服务,在 DbMigratorHostedService 类中包含 StartAsync()和 StopAsync()这 2 个方法,在 StartAsync()中获取 BookStore 数据库迁移服务 BookStoreDbMigrationService,并执行数据库迁移方法 MigrateAsync()。数据库迁移的思路基本上就是在 Acme.BookStore.DbMigrator 目录下执行命令:dotnet ef migrations add InitialCreate 和 dotnet ef database update,只不过使用的 C#代码来实现的。自己通过 Acme.BookStore.DbMigrator 项目没有迁移成功,最后还是通过命令行实现迁移的。迁移结果如下:
说明: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)