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

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Microsoft Azure Blog
Microsoft Azure Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Webroot Blog
Webroot Blog
腾讯CDC
The Last Watchdog
The Last Watchdog
博客园 - 司徒正美
H
Hacker News: Front Page
I
InfoQ
A
Arctic Wolf
H
Hackread – Cybersecurity News, Data Breaches, AI and More
H
Heimdal Security Blog
L
LINUX DO - 最新话题
T
Threat Research - Cisco Blogs
宝玉的分享
宝玉的分享
Last Week in AI
Last Week in AI
Security Latest
Security Latest
D
DataBreaches.Net
C
Check Point Blog
J
Java Code Geeks
www.infosecurity-magazine.com
www.infosecurity-magazine.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Attack and Defense Labs
Attack and Defense Labs
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
L
Lohrmann on Cybersecurity
雷峰网
雷峰网
Vercel News
Vercel News
WordPress大学
WordPress大学
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Spread Privacy
Spread Privacy
Forbes - Security
Forbes - Security
阮一峰的网络日志
阮一峰的网络日志
Hacker News: Ask HN
Hacker News: Ask HN
大猫的无限游戏
大猫的无限游戏
I
Intezer
N
News and Events Feed by Topic
小众软件
小众软件
B
Blog RSS Feed
Help Net Security
Help Net Security
Google DeepMind News
Google DeepMind News
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 三生石上(FineUI控件)
S
Security @ Cisco Blogs
美团技术团队
Recent Announcements
Recent Announcements
Martin Fowler
Martin Fowler
Engineering at Meta
Engineering at Meta
The GitHub Blog
The GitHub Blog
MyScale Blog
MyScale Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main

元视角

.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 中实体和服务的扩展技巧,实体扩展解决了如何为数据库表添加扩展字段的问题,服务扩展解决了如何为默认服务扩展功能的问题,尤其是后者,依赖注入在其中扮演着无比重要的角色。果然,这世上的事情,只有你真正在乎的时候,你才会愿意去承认,那些你曾经轻视过的东西,也许,它们是对的吧!好了,以上就是这篇博客的全部内容,欢迎大家在评论区留言,喜欢的话请记得点赞、收藏、一键三连。