惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Privacy International News Feed
V
Vulnerabilities – Threatpost
The Last Watchdog
The Last Watchdog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
O
OpenAI News
T
Threat Research - Cisco Blogs
WordPress大学
WordPress大学
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
P
Palo Alto Networks Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
H
Help Net Security
P
Proofpoint News Feed
MyScale Blog
MyScale Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
T
The Blog of Author Tim Ferriss
H
Hackread – Cybersecurity News, Data Breaches, AI and More
S
Securelist
Vercel News
Vercel News
S
Security Affairs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
B
Blog RSS Feed
云风的 BLOG
云风的 BLOG
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Blog — PlanetScale
Blog — PlanetScale
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Last Week in AI
Last Week in AI
博客园_首页
Attack and Defense Labs
Attack and Defense Labs
G
Google Developers Blog
T
Tor Project blog
Project Zero
Project Zero
腾讯CDC
Schneier on Security
Schneier on Security
月光博客
月光博客
N
Netflix TechBlog - Medium
AWS News Blog
AWS News Blog
L
LINUX DO - 最新话题
P
Proofpoint News Feed
博客园 - 司徒正美
A
About on SuperTechFans
Latest news
Latest news
Scott Helme
Scott Helme
Hacker News: Ask HN
Hacker News: Ask HN
T
Threatpost
Hacker News - Newest:
Hacker News - Newest: "LLM"
C
CERT Recently Published Vulnerability Notes
Google DeepMind News
Google DeepMind News
博客园 - 聂微东

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角 .NET Core 原生 DI 扩展之基于名称的注入实现 - 元视角
ABP vNext 的实体与服务扩展技巧分享 - 元视角
飞鸿踏雪 · 2021-04-19 · via 元视角

使用 ABP vNext 有一个月左右啦,这中间最大的一个收获是:ABP vNext 的开发效率真的是非常好,只要你愿意取遵循它模块化、DDD 的设计思想。因为官方默认实现了身份、审计、权限、定时任务等等的模块,所以,ABP vNext 是一个开箱即用的解决方案。通过脚手架创建的项目,基本具备了一个专业项目该有的“五脏六腑”,而这可以让我们专注于业务原型的探索。例如,博主是尝试结合 Ant Design Vue 来做一个通用的后台管理系统。话虽如此,我们在使用 ABP vNext 的过程中,还是希望可以针对性地对 ABP vNext 进行扩展,毕竟 ABP vNext 无法 100% 满足我们的使用要求。所以,在今天这篇博客中,我们就来说说 ABP vNext 中的扩展技巧,这里主要是指实体扩展和服务扩展这两个方面。我们经常在讲“开闭原则”,可扪心自问,我们每次修改代码的时候,是否真正做到了“对扩展开放,对修改关闭”呢? 所以,在面对扩展这个话题时,我们不妨来一起看看 ABP vNext 中是如何实践“开闭原则”。

扩展实体

首先,我们要说的是扩展实体,什么是实体呢?这其实是领域驱动设计(DDD)中的概念,相信对于实体、聚合根和值对象,大家早就耳熟能详了。在 ABP vNext 中,实体对应的类型为Entity,聚合根对应的类型为AggregateRoot。所以,你可以片面地认为,只要继承自Entity基类的类都是实体。通常,实体都会有一个唯一的标识(Id),所以,订单、商品或者是用户,都属于实体的范畴。不过,按照业务边界上的不同,它们会在核心域、支撑域和通用域三者间频繁切换。而对于大多数系统而言,用户都将是一个通用的域。在 ABP vNext 中,其用户信息由AbpUsers表承载,它在架构上定义了IUser接口,借助于EF Core的表映射支持,我们所使用的AppUser本质上是映射到了AbpUsers表中。针对实体的扩展,在面向数据库编程的业务系统中,一个最典型的问题就是,我怎么样可以给AppUser添加字段。所以,下面我们以AppUser为例,来展示如何对实体进行扩展。

DDD 中的实体、聚合根与值对象 DDD 中的实体、聚合根与值对象

实际上,ABP vNext 中提供了2种方式,来解决实体扩展的问题,它们分别是:Extra Properties基于 EF Core 的表映射。在 官方文档 中,我们会得到更加详细的信息,这里简单介绍一下就好:

对于第1种方式,它要求我们必须实现IHasExtraProperties接口,这样我们就可以使用GetProperty()SetProperty()两个方法,其原理是,将这些扩展字段以JSON格式存储在ExtraProperties这个字段上。如果使用MongoDB这样的非关系型数据库,则这些扩展字段可以单独存储。参考示例如下:

// 设置扩展字段
var user = await _identityUserRepository.GetAsync(userId);
user.SetProperty("Title", "起风了,唯有努力生存");
await _identityUserRepository.UpdateAsync(user);

// 读取扩展字段
var user = await _identityUserRepository.GetAsync(userId);
return user.GetProperty<string>("Title");

可以想象得到,这种方式使用起来没有心智方面的困扰,主要问题是,这些扩展字段不利于关系型数据库的查询。其次,完全以字符串形式存在的键值对,难免存在数据类型的安全性问题。博主的上家公司,在面对这个问题时,采用的方案就是往数据库里加备用字段,从起初的5个,变成后来的10个,最后甚至变成20个,先不说这没完没了的加字段,代码中一直避不开的,其实是各种字符串的Parse/Convert,所以,大家可以自己去体会这其中的痛苦。

基于 EF Core 的表映射

对于第2种方式,主要指 EF Core 里的“表拆分”或者“表共享”,譬如,当我们希望单独创建一个实体SysUser来替代默认的AppUser时,这就是表拆分,因为同一张表中的数据,实际上是被AppUserSysUser共享啦,或者,你可以将其理解为,EF Core配置两个不同的实体时,它们的ToTable()方法都指向了同一张表。这里唯一不同的是,ABP vNext 中提供了一部分方法用来处理问题,因为牵扯到数据库,所以,还是需要“迁移”。下面,我们以给AppUser扩展两个自定义字段为例:

首先,我们给AppUser类增加两个新属性,AvatarProfile:

public class AppUser : FullAuditedAggregateRoot<Guid>, IUser
{
    // ...

    public virtual string Profile { get; private set; }
    public virtual string Avatar { get; private set; }

    //  ...

接下来,按照 EF Core 的“套路”,我们需要配置下这两个新加的字段:

builder.Entity<AppUser>(b =>
{
    // AbpUsers
    // Sharing the same table "AbpUsers" with the IdentityUser
    b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "Users"); 

    b.ConfigureByConvention();
    b.ConfigureAbpUser();

    // Profile
    b.Property(x => x.Profile)
      .HasMaxLength(AppUserConsts.MaxProfileLength)
      .HasColumnName("Profile");
    
    // Avatar
    b.Property(x => x.Avatar)
      .HasMaxLength(AppUserConsts.MaxAvatarLength)
      .HasColumnName("Avatar");
});

接下来,通过MapEfCoreProperty()方法,将新字段映射到IdentityUser实体,你可以理解为,AppUserIdentityUser同时映射到了AbpUsers这张表:

// Avatar
ObjectExtensionManager.Instance.MapEfCoreProperty<IdentityUser, string>(
    nameof(AppUser.Avatar),
    (entityBuilder, propertyBuilder) => {
    propertyBuilder.HasMaxLength(AppUserConsts.MaxAvatarLength);
});

// Profile
ObjectExtensionManager.Instance.MapEfCoreProperty<IdentityUser, string>(
      nameof(AppUser.Profile),
      (entityBuilder, propertyBuilder) => {
      propertyBuilder.HasMaxLength(AppUserConsts.MaxProfileLength);
});

既然,连数据库实体都做了扩展,那么,数据传输对象(DTO)有什么理由拒绝呢?

ObjectExtensionManager.Instance
    .AddOrUpdateProperty<string>(
        new[]
        {
            typeof(IdentityUserDto),
            typeof(IdentityUserCreateDto),
            typeof(IdentityUserUpdateDto),
            typeof(ProfileDto),
            typeof(UpdateProfileDto),
        },
        "Avatar"
    )
    .AddOrUpdateProperty<string>(
        new[]
        {
            typeof(IdentityUserDto),
            typeof(IdentityUserCreateDto),
            typeof(IdentityUserUpdateDto),
            typeof(ProfileDto),
            typeof(UpdateProfileDto)
        },
        "Profile"
    );
});

经过这一系列的“套路”,此时,我们会发现,新的字段已经生效:

ABP vNext 实体扩展效果展示 ABP vNext 实体扩展效果展示

扩展服务

在 ABP vNext 中,我们还可以对服务进行扩展,得益于依赖注入的深入人心,我们可以非常容易地实现或者替换某一个接口,这里则指 ABP vNext 中的应用服务(ApplicationService),例如,CrudAppService类可以帮助我们快速实现枯燥的增删改查,而我们唯一要做的,则是定义好实体的主键(Primary Key)、定义好实体的数据传输对象(DTO)。当我们发现 ABP vNext 中内置的模块或者服务,无法满足我们的使用要求时,我们就可以考虑对原有服务进行替换,或者是注入新的应用服务来扩展原有服务,这就是服务的扩展。在 ABP vNext 中,我们可以使用下面两种方法来对一个服务进行替换:

// 通过[Dependency]和[ExposeServices]实现服务替换
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IIdentityUserAppService))]
public class YourIdentityUserAppService : IIdentityUserAppService, ITransientDependency
{
  //...
}

// 通过ReplaceService实现服务替换
context.Services.Replace(
    ServiceDescriptor.Transient<IIdentityUserAppService, YourIdentityUserAppService>()
);

这里,博主准备的一个示例是,默认的用户查询接口,其返回信息中只有用户相关的字段,我们希望在其中增加角色、组织单元等关联信息,此时。我们可以考虑实现下面的应用服务:

public interface IUserManageAppService
{
    Task<PagedResultDto<UserDetailQueryDto>> GetUsersWithDetails(
      GetIdentityUsersInput input
    );
}

首先,我们定义了IUserManageAppService接口,它含有一个分页查询的方法GetUsersWithDetails()。接下来,我们来考虑如何实现这个接口。需要说明的是,在 ABP vNext 中,仓储模式的支持由通用仓储接口IRepository<TEntity, TKey>提供,ABP vNext 会在AddDefaultRepositories()方法中为每一个聚合根注入对应的仓储。同样地,你可以按照个人喜好为指定的实体注入对应的仓储。由于ABP vNext 同时支持 EF CoreDapperMongoDB,所以,我们还可以使用EfCoreRepositoryDapperRepository 以及 MongoDbRepository,它们都是IRepository的具体实现类。在下面的例子中,我们使用的是EfCoreRepository这个类。

事实上,这里注入的EfCoreIdentityUserRepositoryEfCoreIdentityRoleRepository 以及 EfCoreOrganizationUnitRepository,都是EfCoreRepository的子类,这使得我们可以复用 ABP vNext 中关于身份标识的一切基础设施,来实现不同于官方的业务逻辑,而这就是我们所说的服务的扩展。

[Authorize(IdentityPermissions.Users.Default)]
public class UserManageAppService : ApplicationService, IUserManageAppService
{
    private readonly IdentityUserManager _userManager;
    private readonly IOptions<IdentityOptions> _identityOptions;
    private readonly EfCoreIdentityUserRepository _userRepository;
    private readonly EfCoreIdentityRoleRepository _roleRepository;
    private readonly EfCoreOrganizationUnitRepository _orgRepository;

    public UserManageAppService(
        IdentityUserManager userManager,
        EfCoreIdentityRoleRepository roleRepository,
        EfCoreIdentityUserRepository userRepository,
        EfCoreOrganizationUnitRepository orgRepository,
        IOptions<IdentityOptions> identityOptions
    )
    {
        _userManager = userManager;
        _orgRepository = orgRepository;
        _userRepository = userRepository;
        _roleRepository = roleRepository;
        _identityOptions = identityOptions;
    }

    [Authorize(IdentityPermissions.Users.Default)]
    public async Task<PagedResultDto<UserDetailQueryDto>> GetUsersWithDetails(
      GetIdentityUsersInput input
    )
    {
        //Users
        var total = await _userRepository.GetCountAsync(input.Filter);
        var users = await _userRepository.GetListAsync(
          input.Sorting, 
          input.MaxResultCount, 
          input.SkipCount, 
          input.Filter, 
          includeDetails: true
        );

        //Roles
        var roleIds = users
          .SelectMany(x => x.Roles)
          .Select(x => x.RoleId)
          .Distinct()
          .ToList();
        var roles = await _roleRepository
          .WhereIf(roleIds.Any(), x => roleIds.Contains(x.Id))
          .ToListAsync();

        //OrganizationUnits
        var orgIds = users
          .SelectMany(x => x.OrganizationUnits)
          .Select(x => x.OrganizationUnitId)
          .Distinct()
          .ToList();
        var orgs = await _orgRepository
          .WhereIf(orgIds.Any(), x => orgIds.Contains(x.Id))
          .ToListAsync();

        var items = ObjectMapper.Map<List<Volo.Abp.Identity.IdentityUser>, List<UserDetailQueryDto>>(users);

        foreach (var item in items)
        {
            foreach (var role in item.Roles)
            {
                var roleInfo = roles.FirstOrDefault(x => x.Id == role.RoleId);
                if (roleInfo != null)
                    ObjectMapper.Map(roleInfo, role);
            }

            foreach (var org in item.OrganizationUnits)
            {
                var orgInfo = orgs.FirstOrDefault(x => x.Id == org.OrganizationUnitId);
                if (orgInfo != null)
                    ObjectMapper.Map(orgInfo, org);
            }
        }

        return new PagedResultDto<UserDetailQueryDto>(total, items);
    }
}

这里做一点补充说明,应用服务,即ApplicationService类,它集成了诸如ObjectMapperLoggerFactoryGuidGenerator、国际化、AsyncExecuter等等的特性,继承该类可以让我们更加得心应手地编写代码。曾经,博主写过一篇关于“动态API”的博客,它可以为我们免去从 Service 到 Controller 的这一层封装,当时正是受到了ABP 框架的启发。当博主再次在 ABP vNext 中看到这个功能时,不免会感慨逝者如斯,而事实上,这个功能真的好用,真香!下面是经过改造以后的用户列表。考虑到,在上一篇博客里,博主已经同大家分享过分页查询方面的实现技巧,这里就不再展开讲啦!

对“用户服务”进行扩展 对“用户服务”进行扩展

本文小结

我们时常说,"对修改关闭,对扩展开放","单一职责",可惜这些原则最多就出现在面试环节。当你接触了真实的代码,你会发现"修改“永远比”扩展“多,博主曾经就见到过,一个简单的方法因为频繁地”打补丁",最后变得面目全非。其实,有时候并不是维护代码的人,不愿意去"扩展",而是写出可"扩展“的代码会更困难一点,尤其是当所有人都不愿意去思考,一味地追求短平快,这无疑只会加速代码的腐烂。在这一点上,ABP vNext 提供了一种优秀的范例,这篇文章主要分享了 ABP vNext 中实体和服务的扩展技巧,实体扩展解决了如何为数据库表添加扩展字段的问题,服务扩展解决了如何为默认服务扩展功能的问题,尤其是后者,依赖注入在其中扮演着无比重要的角色。果然,这世上的事情,只有你真正在乎的时候,你才会愿意去承认,那些你曾经轻视过的东西,也许,它们是对的吧!好了,以上就是这篇博客的全部内容,欢迎大家在评论区留言,喜欢的话请记得点赞、收藏、一键三连。