abp-002手动搭建一个abp模块化单体项目及abp常见问题

2025-08-16

前言

是什么

本文主要介绍如何不依赖 abp cli手动搭建模块化单体abp项目,并记录一下使用abp项目常见的约定和范式以及常见问题。

注意:本项目并没有实际的业务场景,仅仅只是"空项目",具备swagger、自动Api(无需Controller)、仓储功能(SQL Server、dbFirst)。主要适用于以下场景:学习搭建abp项目、找一个可用的abp项目结构然后实现你的业务逻辑

版本:.net 8.0、Abp 8.3.4、SQL Server

源码:MyProject v1.0.0

为什么

abp可以通过cli工具创建项目,确实很全面也很好用,但为什么还要手动搭建abp项目呢?我给出以下原因

  • abp cli最新版本(0.7.0+)需要团队许可证才支持--tiered模式

  • abp cli创建的项目很全面,包含用户、权限、角色等很多功能,但有时候我们并不需要这些

  • abp cli创建的项目会初始化很多表格,但有时候我们并不需要这些

  • abp cli创建的项目附带很多(非核心)业务功能,对于初学者学习或了解框架不友好

  • 有时我们只需要其中的一小部分功能,或者只需要abp这个项目架构

  • 教练,我想学习abp框架

本文介绍的是模块化单体项目(Host+Module),包含swagger、自动Api(无需Controller)、仓储功能(SQL Server、dbFirst)

怎么做

如果让你在一个当前项目(现成的abp项目结构)中添加一个业务接口,如用户信息管理加一个新业务,你会怎么做?如书籍管理

  1. Application.Contracts层相关文件夹BookContracts新建一个接口IBookAppService(这个文件夹还会存放Book相关的Dto、Enum)

  2. Application层相关文件夹BookApp新建一个类BookAppService,实现IBookAppService接口

  3. Domain层相关文件夹BookDomain新建一个实体类Book,继承于Entity<int>

  4. EntityFrameworkCore层的DBContext维护该实体DbSet<Book> Books,有时无需这一步,但必须有modelBuilder.Entity<Book>();

  5. Domain层相关文件夹BookDomain新建一个仓储接口IBookRepository,继承IRepository<Book>

  6. EntityFrameworkCore层相关文件夹BookEF新建一个仓储实现BookRepository,继承EF泛型类EfCoreRepository<ModuleDbContext, Book, int>,并实现IBookRepository接口

  7. 基础设施层建好之后,在BookAppService中通过构造函数注入IBookRepository<Book>即可访问仓储层

  8. 增加api接口的顺序,在IBookAppService定义业务api接口,BookAppService重写方法,调用仓储层,实现业务逻辑。

  9. 大部分仓储方法(增删改查)已在IRepository定义,可直接使用。若需要自定义可在IBookRepository定义接口方法,然后由BookRepository实现接口并重写方法

  10. 各个层下最好能用文件夹区分一下业务,命名格式参考:Books或者BookDomain、BookApp

abp项目结构

abp常见项目结构

abp常见项目结构:传统单体、模块化单体(Host+Module)、微服务分层部署的对比。当然模块化单体也支持做成分层部署的方式

维度传统单体(无模块)模块化单体(Host+Module)微服务 / 分层部署 abp --tiered
代码耦合度高(所有代码混在一起)低(模块内高内聚,模块间低耦合)极低(服务独立)
部署复杂度简单(单应用)简单(单应用)复杂(多应用 / 多服务器)
开发效率初期快,后期低(维护困难)全周期较均衡(模块化 + 单体优势)初期低(分布式设计成本高)
性能高(进程内调用)高(进程内调用)较低(grpc跨服务网络开销)
扩展性差(无法单独扩展某功能)中(可整体扩展,模块级扩展有限)高(可单独扩展某服务)

项目结构图

搭建项目结构

快速搭建abp模块化单体项目

  1. 使用vs新建空白解决方案

  2. 在vs解决方案中,创建src文件夹,在src文件夹下创建modules文件夹,在modules文件夹下创建user文件夹,作为user模块

  3. 在文件资源管理器中,找到刚才的解决方案(.sln)所在位置,创建src文件夹,在src文件夹下创建modules文件夹,在modules文件夹下创建user文件夹,存放user模块的文件

  4. 在vs中,右键src文件夹创建MyProject.Host项目(ASP.NET Core Web API),项目位置选择到src即可

  5. 在vs中,右键user文件夹创建MyProject.User.Domain项目(类库),项目位置选择到src\modules\user

  6. 在vs中,右键user文件夹分别创建Application、Application.Contracts、EntityframeworkCore、Shared项目,项目位置选择到src\modules\user

image-20250801174420481

参考:如何在Visual Studio中创建src文件夹?

项目依赖

小技巧:你可以在vs双击项目,编辑项目依赖,然后把以下的xml内容添加到各个项目中,再右键解决方案进行还原Nuget包即可

MyProject.User.Application

依赖Volo.Abp.AutoMapper、Volo.Abp.Ddd.Application包,依赖MyProject.User.Application.Contracts、MyProject.User.Domain项目

MyProject.User.Application.Contracts

依赖Volo.Abp.Ddd.Application.Contracts包,依赖MyProject.User.Shared项目

MyProject.User.Domain

依赖Volo.Abp.Ddd.Domain

MyProject.User.EntityframeworkCore

依赖Volo.Abp.EntityFrameworkCore、Volo.Abp.EntityFrameworkCore.SqlServer包,依赖MyProject.User.Domain项目

MyProject.User.Shared

依赖Volo.Abp.Core

MyProject.Host

依赖Swashbuckle.AspNetCore、Volo.Abp.AspNetCore、.Abp.Autofac、Volo.Abp.Core、Volo.Abp.Swashbuckle包,依赖MyProject.User.Application、MyProject.User.EntityframeworkCore项目

代码实现

添加各个项目的Module文件

后续还会修改的

创建实体

Domian层的UserInfos文件夹下创建实体UserInfo

创建DBContext

EntityframeworkCore层的EntityframeworkCore文件夹下创建数据库上下文UserModuleDbContext

Host层的appsettings.json文件中加入数据库连接字符串配置

注入DBContext

修改EntityframeworkCore层的模块配置UserModuleEntityFrameworkCoreModule,注入数据库上下文信息

无需手动注入,配置即可

定义业务契约接口

Application.Contracts层的UserInfos文件夹定义了业务接口IUserInfoAppService和相关Dto

实现业务接口

Application层的UserInfos文件夹定义业务实现UserInfoAppService,继承ApplicationService,实现IUserInfoAppService

无需手动注入IUserInfoAppService

配置并使用AutoMapper

Application层下创建AutoMapper映射文件UserModuleApplicationAutoMapperProfile

Application层修改module文件UserModuleApplicationModule

定义自定义仓储接口(可选)

Domain层下的UserInfos文件夹定义仓储接口IUserInfoRepository,可以在里面自定义接口以实现不同功能

定义自定义仓储实现(可选)

EntityframeworkCore层的UserInfos文件夹下创建仓储实现UserInfoRepository,可以在里面实现自定义仓储接口以实现不同的功能

无需手动注入IUserInfoRepository

注意:若不自定义仓储,需要在DbContext中显示定义DbSet<Entity>,不然会注入仓储失败

配置和使用abp

Host层的MyProjectHostModule配置abp相关功能——swagger、autofac、丢弃Controller自动api

使用abp和autofac

Host层的Program中使用abp和autofact

常见问题

abp约定

abp毕竟不是从0到1的项目,可能是从0.5到1的项目,大多数还是要靠约定和范式来开发项目,有了这些,才能更方便更高效进行开发

  1. 项目结构按模块划分,每个模块都有单独的应用层、契约层、领域层

  2. 与实体相关的文件夹最好使用复数——UserInfos;每层的公共文件夹以Common结尾——DomainCommon、ApplicationCommon;项目命名方式:[项目名称].[模块名称].层——MyProject.User.Application

  3. 一定要通过[DependsOn(typeof(xxxModule))]显示声明模块依赖,可以避开绝大部分的报错

  4. 所有的Module文件都要继承AbpModule

  5. 跨模块交互通过应用服务接口事件总线,不直接引用其他模块的领域层

  6. 领域层包括实体、聚合根、领域服务(xxxService)、领域事件(xxxEvent)、审计字段;仓储接口(IxxxRepository),继承IRepository接口。实现IDomainService会自动注册为领域服务

  7. 应用层包括应用服务实现(xxxAppService),继承ApplicationService;在应用服务中可以通过特性[UnitOfWork]开启事务

  8. 契约层包括应用应用服务接口(IxxxAppService),继承IApplicationService;各类输入输出Dto

  9. 基础设施层包括数据库上下文(xxxDbContext),仓储实现(xxxRepository),仓储实现继承于EfCoreRepository<UserModuleDbContext, UserInfo, int>

  10. 命名规范:领域服务——xxxService;领域事件——xxxEvent;仓储接口——IxxxRepository;仓储实现——xxxRepository;应用服务接口——IxxxAppService;应用服务实现——xxxAppService;数据库上下文——xxxDbContext;

  11. abp支持autofac自动注入Service、Repository,

  12. 无需手动配置。如有需求,可以继承ITransientDependency、IScopedDependency、ISingletonDependency接口来完成瞬时、作用域、单例注入

  13. abp自带全局异常过滤器,会处理未捕获的异常,统一返回格式。自定义异常可以继承AbpException

  14. 权限命名格式:[模块名].[实体名].[操作]——OrderManagement.Orders.Create

  15. 使用AutoMapper或者ObjectMapper,而不是一一赋值

  16. 无特殊操作,无需创建自定义仓储,可以通过IRepository<UserInfo, int>注入相应实体的仓储

  17. IApplicationService 接口的返回值,会被自动包装为统一格式的json响应体,包含 successresulterror 等字段

  18. 分页查询应该继承PagedAndSortedResultRequestDto,分页结果则使用IPagedResult<T>PagedResultDto<T>

  19. ABP 会自动记录关键操作的审计日志,无需手动编写日志代码,包含操作人、操作时间、执行方法、参数、耗时、IP 地址等。可以通过[DisableAuditing]关闭

  20. 实体支持多租户(IMultiTenant 接口),软删除(ISoftDelete 接口)

  21. 可以配置丢弃Controller,自动api,支持将方法名自动映射HTTP 方法:Getxxx——Get、Createxxx——Post,Updatexxx——Put,Deletexxx——Delete

swagger不显示UserInfoAppService的api接口

当swagger不显示AppService的api接口,swagger提示No operations defined in spec!。我们可以检查这几个地方:

  • UserInfoAppService要继承ApplicationService基类或实现IApplicationService接口

  • UserInfoAppService类或方法不能用[RemoteService(IsEnabled = false)]修饰

  • UserInfoAppService类或方法只能用public访问类型修饰

  • Host项目,必须配置AddAbpSwaggerGen,而且AddAbpSwaggerGen必须要加上options.DocInclusionPredicate((docName, description) => true);

  • Host项目,必须配置AbpAspNetCoreMvcOptions

  • Host项目,必须使用中间件UseSwaggerUseSwaggerUI

完整MyProjectHostModule如下:

swagger不显示模块的注释信息

问题:未指定xml文档文件

解决方案:在Application增加xml文档文件,并手动加载xml文件,如下

  1. 先增加xml文件——MyProject.User.Application.xml

项目右键-属性-找到输出区域-点击勾选文档文件-点击浏览,没有xml则创建一个空文件(记得选择到bin目录下的debug文件夹,release模式会自动生成到release文件夹,无需再次配置)

image-20250805105536391

  1. MyProjectHostModule类要依赖UserApplication模块, DepensOn(typeof(UserModuleApplicationModule))

  2. 在MyProjectHostModule类AddAbpSwaggerGen方法中,手动加载上述的xml文件

DBContext注入不成功

手动搭建框架时遇到一个很基本的问题:调用仓储Repository时一直报空指针异常,调试才发现原来时DBContext未注入成功(构造方法的断点都进不去)导致内部方法(await GetDbContextAsync()).Set<TEntity>()抛出空指针异常(GetDbContextAsync()为null)

image-20250807114622578

解决方案:在Program中使用Autofac,并且Host模块的DepensOn必须引入Autofac

image-20250807114902913

为什么AppService接口、Repository仓储接口都可以自动注入成功,唯独DbContext无法自动注入?必须得要引入Autofac才可以。而且注入不成功也应该给个合理的错误或者提示吧,直接报空指针异常,我也知道时dbcontext的问题,但就是无从下手。

这个问题卡了两天了,对照其他abp项目愣是看不出问题,我就纳闷了为什么同样的写法,其他项目可以运行,我的却不行。最后花了100块在闲鱼处理,才知道原来是Program没有使用autofac模块

abp的Depenson很重要,有时候代码逻辑看着没问题,程序仍然报错,很大概率是DepensOn的问题。不过我们很难从报错和异常信息中定位是哪个DepensOn出问题,有时候甚至不知道少了哪些模块,修复很困难,对于小白无疑是一个棘手的问题。

如果你在abp项目碰到一个问题,无论怎么调整代码逻辑都不起作用,这时可以多多关注DepensOn,很有可能是DepensOn的问题

报错'The requested service UnitOfWorkInterceptor xxx has not been registered'

The requested service 'Volo.Abp.Castle.DynamicProxy.AbpAsyncDeterminationInterceptor`1[[Volo.Abp.Uow.UnitOfWorkInterceptor, Volo.Abp.Uow, Version=8.3.4.0, Culture=neutral, PublicKeyToken=null]]' has not been registered

问题:Program使用了Autofac相关内容,但Host层没有指定依赖AbpAutofacModule模块

解决方案:去掉Autofac相关内容或者在Host层的DepensOn加入typeof(AbpAutofacModule)即可

image-20250807114427188

报错'Cannot create a DbSet for xxx because this type is not included'

Cannot create a DbSet for 'UserInfo' because this type is not included in the model for the context

问题:未识别到实体

解决方案:在DbContext指明实体DbSet或者在OnModelCreating方法中配置实体映射,选其一即可,推荐后者

image-20250807112137994

报错'using the connection to database 'xxxx''

An error occurred using the connection to database 'xxxx' on server 'xxxxxx'

问题:数据库字符串链接错误

解决方案:调整链接字符串内容或检查ConnectionStringName是否正确。注意ConnectionStringName指的是ConnectionStrings下的UserModule

image-20250807112440939

报错'None of the constructors found on type xxxDbContext'

None of the constructors found on type 'MyProject.User.EntityFrameworkCore.UserModuleDbContext' can be invoked with the available services and parameters

问题:没有指定注入DbContext模块

解决方案:在xxxEntityFrameworkCoreModule加入以下代码即可

报错'None of the constructors found on type xxxRepository'

None of the constructors found on type 'MyProject.User.EntityframeworkCore.Users.UserInfoRepository' can be invoked with the available services and parameters

问题:EntityFrameworkCore层没有指定依赖数据库模块

解决方案:在xxxEntityFrameworkCoreModule的DependsOn加入数据库模块即可,如:typeof(AbpEntityFrameworkCoreSqlServerModule)。根据不同的数据库引入相应数据库模块

image-20250807113158081

还有个地方可能会引发这个问题:在UserModuleDbContext中,如果不显示定义DBSet且没有任何的自定义IxxxRepository,那么就会报错。一句话就是abp识别和自动注入仓储未成功

解决方案:在UserModuleDbContext中显示定义DBSet或者加一个自定义的IxxxRepository即可解决问题

报错'None of the constructors found on type 'Castle.Proxies.xxxAppServiceProxy'

None of the constructors found on type 'Castle.Proxies.UserInfoAppServiceProxy' can be invoked with the available services and parameters

问题:Host层没有指定依赖EntityFrameworkCore层

解决方案:在ProjectHostModule的DependsOn加入仓储层即可,如:typeof(UserModuleEntityFrameworkCoreModule)

image-20250807114255393

报错'Error mapping types'

AutoMapper.AutoMapperMappingException: Error mapping types.

AutoMapper.AutoMapperMappingException: Missing type map configuration or unsupported mapping.

Mapping types: UserInfo -> UserInfoDto

问题:缺少Automapper映射、未配置对象映射、未依赖AutoMapper模块

解决方案:应用层添加Automapper映射文件、应用层配置对象映射、应用层依赖AutoMapper模块

报错'The requested service IApplicationBuilder xxx has not been registered'

The requested service 'Volo.Abp.DependencyInjection.ObjectAccessor`1[[Microsoft.AspNetCore.Builder.IApplicationBuilder, Microsoft.AspNetCore.Http.Abstractions, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60]]' has not been registered

问题:Program中初始化abp应用报错——app.InitializeApplication();

解决方案:Host层的DepensOn必须依赖AbpAspNetCoreModule模块。也可以依赖AbpSwashbuckleModuleAbpAspNetCoreMvcModule,因为这些最终都会依赖AbpAspNetCoreModule

image-20250807144111099

DisableAuditing与DisableValidation

[DisableAuditing, DisableValidation] 常用于修饰类或方法,用于禁用ABP自带的审计日志功能和自动数据验证功能

ABP 框架默认会对应用服务方法进行审计日志记录,包括调用者、调用时间、参数等信息

ABP 会自动对应用服务方法的输入参数进行数据验证(基于 DataAnnotations 或自定义验证器)

domain公共方法应该放在哪里?

对于多个领域(Domain)都需要共享的公共类或方法(如ADomain和BDomain都会用到某个方法C),建议优先放在 Domain 层的 Common 文件夹中,而非 Shared

Shared层,一般用于存放跨层共享的基础设施代码(如通用工具类、常量、枚举、扩展方法等),这些代码不依赖于具体业务领域,具有更广泛的通用性(例如:字符串工具、日期处理、权限常量等)