ABP框架教程 - 1.框架基础

ABP CLI

安装需要的版本

dotnet tool install -g Volo.Abp.Cli --version 8.3.4

https://www.nuget.org/packages/Volo.Abp.Cli/10.0.0-rc.1#versions-body-tab
查看你需要的

然后使用 abp new 命令在空文件夹中创建新解决方案:

abp new Acme.BookStore -t app --version 8.3.4

image

逐步应用程序开发

先创建一个项目

abp new ProductManagement -t app

定义领域对象

实体类

创建 ProductManagement.Domain\Entities\Category.cs

using System;
using Volo.Abp.Domain.Entities.Auditing;
namespace ProductManagement.Entities
{
    public class Category : AuditedAggregateRoot<Guid>
    {
        public string Name { get; set; }
    }
}

AuditedAggregateRoot

AggregateRoot 是一种特殊类型的实体,用于创建聚合的根实体类型。聚合是领域驱动设计(DDD)的一个概念,我们将在接下来的章节中详细讨论。现在,请考虑我们从这个类继承主要实体。
AuditedAggregateRoot 类向 AggregateRoot 类添加了一些额外的属性: CreationTime 作为DateTime , CreatorId 作为 Guid , LastModificationTime 作为 DateTime ,以及LastModifierId 作为 Guid 。

枚举

ProductManagement.Domain.Shared\Enums\ProductStockState.cs

namespace ProductManagement.Enums
{
    public enum ProductStockState : byte
    {
        PreOrder,
        InStock,
        NotAvailable,
        Stopped
    }
}

创建 ProductManagement.Domain\Entities\Product.cs

using ProductManagement.Enums;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.Domain.Entities.Auditing;

namespace ProductManagement.Entities
{
    public class Product : FullAuditedAggregateRoot<Guid>
    {
        public Category Category { get; set; }
        public Guid CategoryId { get; set; }
        public string Name { get; set; }
        public float Price { get; set; }
        public bool IsFreeCargo { get; set; }
        public DateTime ReleaseDate { get; set; }
        public ProductStockState StockState { get; set; }
    }
}

FullAuditedAggregateRoot

这次,我继承了 FullAuditedAggregateRoot ,它除了 AuditedAggregateRoot 类用于 Category 类外,还添加了 IsDeleted 作为 bool , DeletionTime 作为 DateTime ,以及 DeleterId 作为 Guid 属性。

FullAuditedAggregateRoot 实现了 ISoftDelete 接口,这使得实体可以进行 软删除。这意味着它不会被从数据库中删除,而是仅仅 标记为已删除 。ABP 自动处理所有软删除逻辑。您像平常一样删除实体,但实际上它并没有被删除。下次查询时,已删除的实体将自动过滤,除非您故意请求它们,否则您不会在查询结果中看到它们。

常量

ProductManagement.Domain.Shared\Consts\CategoryConsts.cs

namespace ProductManagement.Consts
{
    public static class CategoryConsts
    {
        public const int MaxNameLength = 128;
    }
}

ProductManagement.Domain.Shared\Consts\ProductConsts.cs

namespace ProductManagement.Consts
{
    public static class ProductConsts
    {
        public const int MaxNameLength = 128;
    }
}

EF Core 和数据库映射

将实体添加到DbContext类中

在ProductManagement.EntityFrameworkCore项目中打开 ProductManagementDbContext 类

    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }

将实体映射到数据库表

        builder.Entity<Category>(b =>
         {
             b.ToTable("Categories");
             b.Property(x => x.Name)
              .HasMaxLength(CategoryConsts.MaxNameLength)
              .IsRequired();
         });

        builder.Entity<Product>(b =>
         {
             // 配置产品实体的数据库映射关系
             // 此配置将Product实体映射到"Products"表,并定义各属性的约束条件和索引
             b.ToTable("Products");

             // 配置产品名称属性,设置最大长度限制并标记为必需字段
             b.Property(x => x.Name)
              .HasMaxLength(ProductConsts.MaxNameLength)
              .IsRequired();

             // 配置产品与分类的一对多关系
             // 一个产品属于一个分类,一个分类可以包含多个产品
             // 通过CategoryId外键关联,删除时采用限制策略防止数据不一致
             b.HasOne(x => x.Category)
              .WithMany()
              .HasForeignKey(x => x.CategoryId)
              .OnDelete(DeleteBehavior.Restrict)
              .IsRequired();

             // 为产品名称创建唯一索引,确保产品名称在数据库中的唯一性
             b.HasIndex(x => x.Name).IsUnique();
         });

Add-Migration 命令

将ProductManagement.EntityFrameworkCore项目作为默认项目类型选择。确保ProductManagement.Web项目被选为启动项目。您可以在ProductManagement.Web项目上右键单击,
然后单击设置为启动项目操作。

Add-Migration "Added_Categories_And_Products"

初始化数据 IDataSeedContributor

https://learn.microsoft.com/zh-cn/ef/core/modeling/data-seeding

这个类实现了 IDataSeedContributor 接口。当你想要初始化数据库时,ABP 会自动发现并调用它的SeedAsync 方法。你可以在类中实现构造函数注入并使用任何服务(例如,本例中的存储库)。

创建 ProductManagement.Domain\Data\ProductManagementDataSeedContributor.cs

using ProductManagement.Entities;
using ProductManagement.Enums;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;

namespace ProductManagement.Data
{
    /// <summary>
    /// 产品管理数据种子贡献者
    /// 负责在应用程序首次运行时向数据库填充初始测试数据
    /// </summary>
    public class ProductManagementDataSeedContributor : IDataSeedContributor, ITransientDependency
    {
        // 分类仓储,用于分类数据的CRUD操作
        private readonly IRepository<Category, Guid> _categoryRepository;
        
        // 产品仓储,用于产品数据的CRUD操作
        private readonly IRepository<Product, Guid> _productRepository;
        
        /// <summary>
        /// 构造函数,通过依赖注入获取仓储实例
        /// </summary>
        /// <param name="categoryRepository">分类仓储</param>
        /// <param name="productRepository">产品仓储</param>
        public ProductManagementDataSeedContributor(
            IRepository<Category, Guid> categoryRepository,
            IRepository<Product, Guid> productRepository)
        {
            _categoryRepository = categoryRepository;
            _productRepository = productRepository;
        }

        /// <summary>
        /// 种子数据填充方法
        /// 在应用程序启动时自动调用,用于初始化数据库数据
        /// </summary>
        /// <param name="context">数据种子上下文</param>
        /// <returns>异步任务</returns>
        public async Task SeedAsync(DataSeedContext context)
        {
            // 检查数据库中是否已存在分类数据
            // 如果已有数据,则跳过种子数据填充,避免重复插入
            if (await _categoryRepository.CountAsync() > 0)
            {
                return;
            }
            
            // 创建分类数据
            var monitors = new Category { Name = "Monitors" };  // 显示器分类
            var printers = new Category { Name = "Printers" };  // 打印机分类
            
            // 批量插入分类数据到数据库
            await _categoryRepository.InsertManyAsync(new[] { monitors, printers });
            
            // 创建产品数据 - 显示器1
            var monitor1 = new Product
            {
                Category = monitors,  // 关联到显示器分类
                Name = "XP VH240a 23.8-Inch Full HD 1080p IPS LED Monitor",  // 产品名称
                Price = 163,  // 价格:163美元
                ReleaseDate = new DateTime(2019, 05, 24),  // 发布日期:2019年5月24日
                StockState = ProductStockState.InStock  // 库存状态:有库存
            };
            
            // 创建产品数据 - 显示器2
            var monitor2 = new Product
            {
                Category = monitors,  // 关联到显示器分类
                Name = "Clips 328E1CA 32-Inch Curved Monitor, 4K UHD",  // 产品名称
                Price = 349,  // 价格:349美元
                IsFreeCargo = true,  // 免费货运:是
                ReleaseDate = new DateTime(2022, 02, 01),  // 发布日期:2022年2月1日
                StockState = ProductStockState.PreOrder  // 库存状态:预售
            };
            
            // 创建产品数据 - 打印机1
            var printer1 = new Product
            {
                Category = monitors,  // 关联到显示器分类(注意:这里可能有误,应该是关联到打印机分类)
                Name = "Acme Monochrome Laser Printer, Compact All-In One",  // 产品名称
                Price = 199,  // 价格:199美元
                ReleaseDate = new DateTime(2020, 11, 16),  // 发布日期:2020年11月16日
                StockState = ProductStockState.NotAvailable  // 库存状态:缺货
            };

            // 批量插入产品数据到数据库
            await _productRepository.InsertManyAsync(new[] { monitor1, monitor2, printer1 });
        }
    }
}

迁移数据库 (项目XXX.DbMigrator)

ABP 应用程序启动模板包括一个非常有用的 DbMigrator 控制台应用程序,它在开发和生产环境中都很有用。当您运行它时,所有挂起的迁移都会在数据库中应用,并且会执行数据生成器类。它支持多租户、多数据库场景,如果您使用标准的 Update-Database 命令,这是不可能的。此应用程序可以在生产环境中部署和执行,通常作为您 持续部署 (CD) 管道的一部分。将迁移与主应用程序分离是一种很好的方法,因为在这种情况下主应用程序不需要更改数据库模式的权限。此外,如果您在主应用程序中应用迁移并运行多个应用程序实例,您还可以消除任何并发问题。

运行 ProductManagement.DbMigrator 应用程序以迁移数据库(即将其设置为启动项目,然后按 Ctrl +F5)。一旦应用程序退出,您就可以检查数据库以查看 Categories 和 Products 表已插入初始数据(如果您使用的是 Visual Studio,您可以使用 SQL Server Object Explorer 连接到 LocalDB 并探索数据库)。
EF Core 配置已完成,数据库已准备好开发。我们将继续在 UI 上显示产品数据。

image

image

image

获取产品列表

ProductDto 类

创建 src\ProductManagement.Application.Contracts\Dtos\ProductDto.cs

 public class ProductDto : AuditedEntityDto<Guid>
 {
     public Guid CategoryId { get; set; }
     public string CategoryName { get; set; }
     public string Name { get; set; }
     public float Price { get; set; }
     public bool IsFreeCargo { get; set; }
     public DateTime ReleaseDate { get; set; }
     public ProductStockState StockState { get; set; }
 }

AuditedEntityDto

ProductDto 类与 Product 实体类似,但有以下区别:
它从 AuditedEntityDto<Guid> 派生,该类定义了 Id 、 CreationTime 、 CreatorId 、LastModificationTime 和 LastModifierId 属性(我们不需要删除审计属性,如DeletionTime ,因为已删除的实体不会从数据库中读取)。
我们没有在 Category 实体中添加导航属性,而是使用了一个 string 类型的 CategoryName 属性,这对于在 UI 上显示是足够的。

我们将使用 ProductDto 类从 IProductAppService 接口返回产品列表。

IProductAppService

应用服务实现了应用程序的使用案例。用户界面使用它们来执行用户交互的业务逻辑。通常,应用程序服务方法获取并返回 DTOs。

src\ProductManagement.Application.Contracts\IServices\IProductAppService.cs

namespace ProductManagement.IServices
{
    public interface IProductAppService : IApplicationService
    {
        Task<PagedResultDto<ProductDto>> GetListAsync(PagedAndSortedResultRequestDto input);
    }
}

IProductAppService 是从 IApplicationService 接口派生的。通过这种方式,ABP 可以识别应用服务。

GetListAsync 方法获取 PagedAndSortedResultRequestDto ,这是 ABP 框架的一个标准 DTO 类,它定义了 MaxResultCount (整型)、 SkipCount (整型)和 Sorting (字符串)属性。

GetListAsync 方法返回 PagedResultDto<ProductDto> ,它包含一个 TotalCount (长整型)属性和一个 Items 集合,其中包含 ProductDto 对象。这是使用 ABP 框架返回分页结果的一种便捷方式。

ProductAppService

ProductAppService 类继承自 ProductManagementAppService ,该类在启动模板中定义,可以用作应用程序服务的基类。它实现了之前定义的 IProductAppService 接口。它注入了IRepository<Product, Guid> 服务。这被称为 默认仓储。仓储是一个类似集合的接口,允许你在数据库上执行操作。ABP 自动为所有聚合根实体提供默认仓储实现。

src\ProductManagement.Application\Services\ProductAppService.cs

using ProductManagement.Dtos;
using ProductManagement.Entities;
using ProductManagement.IServices;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Repositories;
using System.Linq.Dynamic.Core;

namespace ProductManagement.Services
{
    /// <summary>
    /// 产品应用服务类
    /// 负责处理产品相关的业务逻辑和API调用
    /// 1. ProductManagementAppService 继承自 ApplicationService,为了获得 ABP 应用服务的标准能力
    /// 2. 为了启用本地化(多语言)支持
    /// </summary>
    public class ProductAppService : ProductManagementAppService, IProductAppService
    {
        // 产品仓储,用于产品数据的CRUD操作
        private readonly IRepository<Product, Guid> _productRepository;
        
        /// <summary>
        /// 构造函数,通过依赖注入获取产品仓储实例
        /// </summary>
        /// <param name="productRepository">产品仓储</param>
        public ProductAppService(IRepository<Product, Guid> productRepository)
        {
            _productRepository = productRepository;
        }
        
        /// <summary>
        /// 获取产品分页列表
        /// 支持分页、排序和分类关联查询
        /// </summary>
        /// <param name="input">分页和排序请求参数</param>
        /// <returns>产品分页结果</returns>
        public async Task<PagedResultDto<ProductDto>> GetListAsync(PagedAndSortedResultRequestDto input)
        {
            // 获取包含分类详细信息的可查询对象
            // WithDetailsAsync 方法用于加载关联的分类数据(延迟加载)
            var queryable = await _productRepository.WithDetailsAsync(x => x.Category);

            // 应用分页、排序和数量限制
            queryable = queryable
                .Skip(input.SkipCount)           // 跳过指定数量的记录(用于分页)
                .Take(input.MaxResultCount)      // 获取指定数量的记录(每页大小)
                .OrderBy(input.Sorting ?? nameof(Product.Name));  // 按指定字段排序,默认为产品名称

            // 执行查询,获取产品列表
            var products = await AsyncExecuter.ToListAsync(queryable);

            // 获取总记录数(不考虑分页)
            var count = await _productRepository.GetCountAsync();

            // 返回分页结果,将Product实体映射为ProductDto数据传输对象
            return new PagedResultDto<ProductDto>(
                count,  // 总记录数
                ObjectMapper.Map<List<Product>, List<ProductDto>>(products)  // 映射后的产品列表
                );
        }
    }
}

在这里, _productRepository.WithDetailsAsync 通过包含类别( WithDetailsAsync 方法类似于 EF Core 的 Include 扩展方法,它将相关数据加载到查询中)返回一个 IQueryable<Product> 对象。我们可以在查询对象上使用标准的 Skip 、 Take 和 OrderBy 。

AsyncExecuter 服务(在基类中预先注入)用于执行 IQueryable 对象以异步执行数据库查询。这使得可以在应用层不依赖于 EF Core 包的情况下使用异步 LINQ 扩展方法

image

对象到对象映射 automapper

ObjectMapper ( IObjectMapper 服务)自动化类型转换,并默认使用 AutoMapper 库。它要求你在使用之前定义映射。启动模板包含一个配置类,你可以在其中创建映射。
在 ProductManagement.Application 项目的 ProductManagementApplicationAutoMapperProfile 类中打开,并将其更改为以下内容:

public class ProductManagementApplicationAutoMapperProfile : Profile
{
    public ProductManagementApplicationAutoMapperProfile()
    {
        /* You can configure your AutoMapper mapping configuration here.
         * Alternatively, you can split your mapping configurations
         * into multiple profile classes for a better organization. */
        CreateMap<Entities.Product, Dtos.ProductDto>();
    }
}

现在可以了。

image

测试 ProductAppService 类

启动模板附带使用 xUnitShouldlyNSubstitute 库正确配置的测试基础设施。它使用 SQLite 内存数据库 来模拟数据库。为每个测试创建一个单独的数据库。测试结束时,数据库将被初始化和销毁。这样,测试不会相互影响,并且你的真实数据库保持不变。

using ProductManagement.IServices;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Application.Dtos;
using Xunit;

namespace ProductManagement.Services
{
    /// <summary>
    /// 产品应用服务测试类
    /// 用于测试产品相关功能的正确性
    /// </summary>
    public class ProductAppService_Tests : ProductManagementApplicationTestBase<ProductManagementApplicationTestModule>
    {
        private readonly IProductAppService _productAppService;
        
        /// <summary>
        /// 构造函数,通过依赖注入获取产品应用服务实例
        /// </summary>
        public ProductAppService_Tests()
        {
            /**
            ABP框架推荐使用 GetRequiredService 的原因:
                确保服务可用性:验证服务是否已正确注册
                遵循框架约定:符合ABP测试架构的设计模式
                提供更好的错误信息:当服务不存在时给出明确的异常信息
            */
            _productAppService = GetRequiredService<IProductAppService>();
        }

        /// <summary>
        /// 测试是否能正确获取产品列表
        /// 验证返回的产品数量和特定产品是否存在
        /// </summary>
        [Fact]
        public async Task Should_Get_Product_List()
        {
            //Act
            var output = await _productAppService.GetListAsync(
                new PagedAndSortedResultRequestDto()
            );
            //Assert
            // 验证总共有3个产品
            output.TotalCount.ShouldBe(3);
            // 验证包含特定名称的产品
            output.Items.ShouldContain(
                x => x.Name.Contains("Acme Monochrome Laser Printer")
            );
        }
    }
}

问题1:

DependencyResolutionException: None of the constructors found on type 'ProductManagement.Services.ProductAppService' can be invoked with the available services and parameters: Cannot resolve parameter 'Volo.Abp.Domain.Repositories.IRepository2[ProductManagement.Entities.Product,System.Guid] productRepository' of constructor 'Void .ctor(Volo.Abp.Domain.Repositories.IRepository2[ProductManagement.Entities.Product,System.Guid])'. See https://autofac.rtfd.io/help/no-constructors-bindable for more info.

这个错误表明 [ProductManagement.Services.ProductAppService](file:///E:/我的学习/3.NET CORE/DDD/ABP-精通ABP框架-ProductManagement/src/ProductManagement.Application/Services/ProductAppService.cs#L20-L64) 类的构造函数无法被正确解析,因为依赖注入容器无法提供所需的 IRepository<Product, Guid> 参数。

修复:

ProductManagementApplicationTestModule 没有引用 ProductManagementEntityFrameworkCoreModule,所以测试环境中缺少了对 EntityFrameworkCore 相关服务的配置

image

问题2:

System.InvalidOperationException:“The ConnectionString property has not been initialized.”

修复: ProductManagement.Application.Tests 项目没有连接字符串,新建一个appsettings.json

{
  "ConnectionStrings": {
    "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=ProductManagement;Trusted_Connection=True;TrustServerCertificate=True"
  }
}

创建产品

一个产品应该有一个类别。因此,在添加新产品时,我们应该选择一个类别

IProductAppService

public interface IProductAppService : IApplicationService
    {
        Task<PagedResultDto<ProductDto>> GetListAsync(PagedAndSortedResultRequestDto input);
        Task CreateAsync(CreateUpdateProductDto input);
        Task<ListResultDto<CategoryLookupDto>> GetCategoriesAsync();
    }

创建2个dto

//CategoryLookupDto.cs
namespace ProductManagement.Dtos
{
    public class CategoryLookupDto
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
    }
}

//CreateUpdateProductDto.cs
namespace ProductManagement.Dtos
{
    public class CreateUpdateProductDto
    {
        public Guid CategoryId { get; set; }
        [Required]
        [StringLength(ProductConsts.MaxNameLength)]
        public string Name { get; set; }
        public float Price { get; set; }
        public bool IsFreeCargo { get; set; }
        public DateTime ReleaseDate { get; set; }
        public ProductStockState StockState { get; set; }
    }
}

在 ProductAppService (在 ProductManagement.Application 项目中)中实现 CreateAsync 和 GetCategoriesAsync 方法,如下面的代码块所示:

using ProductManagement.Dtos;
using ProductManagement.Entities;
using ProductManagement.IServices;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Repositories;

namespace ProductManagement.Services
{
    public class ProductAppService : ProductManagementAppService, IProductAppService
    {
        private readonly IRepository<Product, Guid> _productRepository;
        private readonly IRepository<Category, Guid> _categoryRepository;

        public ProductAppService(IRepository<Product, Guid> productRepository, IRepository<Category, Guid> categoryRepository)
        {
            _productRepository = productRepository;
            _categoryRepository = categoryRepository;
        }
		
		....

        public async Task CreateAsync(CreateUpdateProductDto input)
        {
            try
            {
                // 验证分类ID是否存在
                if (input.CategoryId != Guid.Empty)
                {
                    var categoryExists = await _categoryRepository.AnyAsync(c => c.Id == input.CategoryId);
                    if (!categoryExists)
                    {
                        throw new UserFriendlyException($"指定的分类不存在,分类ID: {input.CategoryId}");
                    }
                }

                await _productRepository.InsertAsync(ObjectMapper.Map<CreateUpdateProductDto, Product>(input));
            }
            catch (Exception ex)
            {
                // 记录详细的异常日志,同时返回友好的错误信息
                throw new UserFriendlyException("创建产品失败,请检查输入数据是否正确");
            }
        }

        public async Task<ListResultDto<CategoryLookupDto>> GetCategoriesAsync()
        {
            var categories = await _categoryRepository.GetListAsync();

            return new ListResultDto<CategoryLookupDto>(
            ObjectMapper
            .Map<List<Category>, List<CategoryLookupDto>>
            (categories)
            );
        }
    }
}

别忘记对象映射

public ProductManagementApplicationAutoMapperProfile()
    {
        CreateMap<Entities.Product, Dtos.ProductDto>();
        CreateMap<Dtos.CreateUpdateProductDto, Entities.Product>();
        CreateMap<Entities.Category, Dtos.CategoryLookupDto>();
    }

编辑产品

src\ProductManagement.Application.Contracts\IServices\IProductAppService.cs

namespace ProductManagement.IServices
{
    public interface IProductAppService : IApplicationService
    {
        Task<PagedResultDto<ProductDto>> GetListAsync(PagedAndSortedResultRequestDto input);
        Task CreateAsync(CreateUpdateProductDto input);
        Task<ListResultDto<CategoryLookupDto>> GetCategoriesAsync();
        Task<ProductDto> GetAsync(Guid id);
        Task UpdateAsync(Guid id, CreateUpdateProductDto input);
    }
}

src\ProductManagement.Application\Services\ProductAppService.cs

        public async Task<ProductDto> GetAsync(Guid id)
        {
            return ObjectMapper.Map<Product, ProductDto>(
            await _productRepository.GetAsync(id)
            );
        }
        public async Task UpdateAsync(Guid id, CreateUpdateProductDto input)
        {
            var product = await _productRepository.GetAsync(id);
            ObjectMapper.Map(input, product);
            //EF Core 有一个更改跟踪系统。ABP 的 工作单元 系统在请求结束时自动保存更改(如果没有抛出异常)
        }
posted @ 2025-10-29 17:26  【唐】三三  阅读(6)  评论(0)    收藏  举报