乐趣区

关于程序员:手动从0搭建ABP框架ABP官方完整解决方案和手动搭建简化解决方案实践

  本文次要解说了如何把 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()办法外,还有一些其它的办法。abp\framework\src\Volo.Abp.Core\Volo\Abp\Modularity\AbpModule.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.Shareddotnet 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<DbMigratorHostedService>()这个数据库迁徙主机服务,在 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/436373…
[2]Abp vNext 源码剖析文章目录:https://www.cnblogs.com/myzon…
[3]手动从 0 搭建 ABP 框架 -ABP 官网残缺解决方案源码:https://url39.ctfile.com/f/25… (拜访明码: 2096)
[4]手动从 0 搭建 ABP 框架 - 手动搭建简化解决方案源码:https://url39.ctfile.com/f/25… (拜访明码: 2096)

本文由 mdnice 多平台公布

退出移动版