经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » ASP.net » 查看文章
我们是如何解决abp身上的几个痛点
来源:cnblogs  作者:张飞洪[厦门]  时间:2025/2/20 10:43:57  对本文有异议

大家好,我是张飞洪,感谢您的阅读,我会不定期和你分享学习心得,希望我的文章能成为你成长路上的垫脚石,让我们一起精进。

abp框架在.net社区是spring一样的存在,用的人也非常多,毫无疑问,它确实是一个不错的框架,不然社区的star也不会那么多。我也是因为它的模块化,ddd,微服务兼容等特点做的选型。但是随着你使用的项目越多,你会发现它也有自己的不足,所谓没有十全十美的框架。

一、abp的痛点

通过使用商业版和社区版的经验,有几个痛点:

1.前端不支持VUE:

angular前端不好招聘,小公司可能个别会ng,但是架不住离职的风险。我曾经见过一个开发团队,后端用abp,前端用ng,前后端不分离,这个时候后端人员就硬着头皮去修改前人的angular,问他为什么改得这么慢,对方说:我比较熟悉vue,老板只有暗暗着急,却无可奈何。显然技术选型出问题,对团队和交付的影响是非常大的;

2.社区版界面比较丑陋:

是不是我的理解有偏差,感觉老外大部分都不怎么重视脸面问题。商业版的界面稍微就好看一些,但是味道感觉还是差了那么一点点;

3.售后服务非常慢:

这个可能是最坑的地方,对新人比较不友好,如果项目紧急,随时可能要面对的是漫长的等待,记得我刚入坑到时候,碰到一个https配置问题,问了邮件许久都没有回复;

4.功能不好用:

社区版的功能要么水土不服,要么功能太浅。看起来好些功能挺多的,但是用起来,还是要改得满头大汗;

5.价格比较昂贵:

带源码商业版要4万3左右,企业版更贵,大概要7.3万。

二、我们如何处理?

我不是说abp商业版不好,它挺不错的,只是不适合自己的业务需求,于是,我们用VUE3+ABP重做了一下(传送门),请大家指教,目前有几个增强点:

1.功能更深入。

比如:

  • 【增强】文件管理:支持多种SSO存储渠道配置和切换;支持图片、文档、视频、音频的存储;
  • 【新增】任务调度:支持单体和集群,支持可视化配置和日志记录;
  • 【重写】认证授权:支持用户池,用户同步,单点登录,目前正在开发中;
  • 【增强】用户和角色扩展;
  • 【复刻】关联账户;
  • 【复刻】委托登录;
  • 【新增】菜单管理;
  • 【新增】行政单位;
  • 【新增】计量单位;
  • 【新增】校验规则;

至于多语言,审计日志, opendidct,多租户等自带的功能,我们用vue3把他们优化和重写了,功能基本没有变化,没有什么可以分享的。

下面是认证授权的同步中心,用来解决多个系统用户集中管理和单点登录问题。

2.细节更深入。

功能层面

比如:菜单支持标星收藏和归类;前后端分离我们基于permission表扩展了菜单表,这样既能支持系统默认菜单,也能支持自定义菜单。

返回格式

又比如,abp的接口返回格式比较没有规则,考虑前端的体验,我们给它包了一个壳,做了格式统一:

  • abp的返回格式:

  • 我们的返回格式:

考虑前端的体验,我们给它包了一个壳

  1. {
  2. "code": null,
  3. "data": {},
  4. "isSuccess": true,
  5. "msg": null,
  6. "extras": null,
  7. "timestamp": 1739241669
  8. }

抛出异常状态码

这里面的状态码也很有讲究,我们观察发现abp也不是很规范,后台抛异常各自情况都有,我们统计了一下,abp的异常种类很多,多到感觉抓不到规律:

  1. Application层:
  2. throw new AbpException($"No policy defined to get/set permissions for the provider '{providerName}'. Use {nameof(PermissionManagementOptions)} to map the policy.");
  3. throw new ApplicationException($"The permission named '{permission.Name}' has not compatible with the provider named '{providerName}'");
  4. throw new AbpValidationException();
  5. throw new FileNotFoundException($"Signing Certificate couldn't found: {file}");
  6. throw new BadHttpRequestException("");
  7. throw new NotImplementedException($"{nameof(GetUserInfoAsync)} is not implemented default. It should be overriden and implemented by the deriving class!");
  8. throw new EntityNotFoundException(typeof(IdentityRole), id);
  9. throw new UserResponseAlreadyExistException();
  10. throw new EmailAddressRequiredException();
  11. Domain层:
  12. throw new BusinessException(code: IdentityErrorCodes.UserSelfDeletion);
  13. throw new BusinessException(EventHubErrorCodes.CantChangeEventTiming)
  14. .WithData("MaxTimingChangeLimit", EventConsts.MaxTimingChangeCountForUser);
  15. throw new BusinessException(EventHubErrorCodes.TrackNameAlreadyExist)
  16. .WithData("Name", name);
  17. throw new ArgumentException("identityResult.Errors should not be null.");
  18. throw new ArgumentNullException(nameof(childCode), "childCode can not be null or empty.");
  19. throw new UserFriendlyException(L["CountryAndCityRequiredForUpdateInPersonEvent"]);
  20. throw new AbpAuthorizationException(
  21. L["EventHub:NotAuthorizedToUpdateEvent", @event.Title],
  22. EventHubErrorCodes.NotAuthorizedToUpdateEvent
  23. );
  24. public static class IdentityErrorCodes
  25. {
  26. public const string UserSelfDeletion = "Volo.Abp.Identity:010001";
  27. public const string MaxAllowedOuMembership = "Volo.Abp.Identity:010002";
  28. public const string ExternalUserPasswordChange = "Volo.Abp.Identity:010003";
  29. public const string DuplicateOrganizationUnitDisplayName = "Volo.Abp.Identity:010004";
  30. public const string StaticRoleRenaming = "Volo.Abp.Identity:010005";
  31. public const string StaticRoleDeletion = "Volo.Abp.Identity:010006";
  32. public const string UsersCanNotChangeTwoFactor = "Volo.Abp.Identity:010007";
  33. public const string CanNotChangeTwoFactor = "Volo.Abp.Identity:010008";
  34. public const string YouCannotDelegateYourself = "Volo.Abp.Identity:010009";
  35. public const string ClaimNameExist = "Volo.Abp.Identity:010021";
  36. }

因为不同的种类异常返回的状态码是有差异的,归类起来有如下业务状态:

  • 请求成功
  • 请求成功,返回空
  • 非法参数
  • 未授权,需提供身份验证
  • 已授权,但服务器拒绝请求
  • 服务器内部错误

到底我们应该如何返回才能减轻前端判断的复杂度,如何让abp的异常和这些码对应起来,如何拦截abp底层异常,进行重写返回码,这是一项挺复杂的任务,后续有机会再做深入分享。

DTO规范

除此之外,abp内部本身的规范也存在一定问题,比如dto的命名,除了以dto为后缀,也会出现以input为后缀的dto类。我们内部统一以input和output作为后缀,因为关于命名我们也有一套规范,这里不展开。dto重要吗,有没有必要拿出来讲,这是一个问题。我觉得dto有很多门道,值得写一篇长文,下面简单点一下

dto门道:

  • dto要不要包含业务逻辑?
  • 未使用的DTO要不要删掉
  • 输入DTO要不要共享
  • 输出DTO要不要共享
  • DTO命名规约(CURD/GET/SAVE/CHECK/LIST/TREE/SELECT/DETAIL等等)
  • DTO如何验证,如何做多语言

不再展开了,敬请期待《DTO应该如何规范比较好?》。

其他

其他的细节还有很多,后续会做一个系列文章《我用abp做企业数字化应用》,这里就不一一罗列,有兴趣的朋友可以通过(传送门)了解学习。

3.基于Ant Design的VUE3框架。

选vue主要考虑vue开发者比较充沛,人员离职交接比较方便;另外颜值这块目前vue生态发展得很给力,应该会更加符合国人口味,更换主体也是标配。好不好看,东哥说我有“眼盲”,不知道奶茶好不好看:

主题萝卜青菜各有所爱,通过比对,不敢也没有对abp不敬,更不是为了输赢,就是想追求品位多一点,不知道这个主题你满意吗?

4.重写abp源码。

框架本身扩展性很不错,但是架不住各自需求不断冲击。
比如:

  • 如何基于用户表,扩展原生的字段?
  • 国内外第三方有几十个,我应该如何集成?
  • 如何在授权中心托管一个VUE前端登录页面?
  • 想给CurrentUser增加一个IsAdmin字段要如何增加?
  • 如何隐藏API接口的显示?
  • 测试阶段,如何取消全部权限Authorize?
  • 如何使用纯净版的abp进行项目开发?
  • 为什么返回的CurrentUser为空?
  • 如何增加用户池Id,模仿IsDelete做数据过滤?
  • 有了控制器层,为什么还要有httpapi这一层?
  • 如何自定义Claim?
  • 为什么老是报实体对象没有注入的错?
  • abp太臃肿,如何给它瘦身?
  • 如何去除abp数据库的表前缀?
  • 微服务底下认证授权如何使用?
  • 如何抛弃abp的identity基础模块,重写opendidct?
  • 如何结合abp,jenkins,docker,k8s做devops?
  • abp的store和领域服务有什么区别?
  • ……
    以上可以继续罗列很多,只要你一直用abp开发,你遇到的坑会越多,你就会越想重写abp,但是当你越过了这些障碍,你就越了解它,知道它的优点和缺点,知道如何避免这些坑位。

下面是我们在做IDaaS这个项目过程中,重写的一个功能,我们给授权中心托管了一个登录组件,支持验证码(手机/邮箱)和密码(手机/邮箱/用户名)登录,这样不管未来你有多少个系统,你都不需要重写登录了。你可以在认证授权中心进行注册应用,选择许可模式,配置scope,你就可以愉快得进行单点登录了。oauth2.0和openid这两个协议想要用好其实挺不容易的:

  • 客户端有很多种类,比如app,web,spa,api等;
  • 许可模式有授权码,隐式,密码和客户端凭证或者自定义模式;
  • 什么类型的客户端要选择什么用的模式,如何自定义模式?

这些东西还是有点复杂的,如果您刚好也在做这类开发,欢迎您留言和我探讨。

5.追求性能极致。

abp在权限列表、权限保存和特征配置列表的性能非常拉胯,比如你定义的功能按钮在100以上,你会发现原生接口的权限渲染和保存会非常卡,主要原因就是在循环里面进行读写,我们花了大力气进行重构,下面是权限设置重构后的代码:

权限列表接口优化

下面是源码内容,里面有四个循环(晕):

  1. public virtual async Task<GetPermissionListResultDto> GetAsync(string providerName, string providerKey)
  2. {
  3. await CheckProviderPolicy(providerName);
  4. var result = new GetPermissionListResultDto
  5. {
  6. EntityDisplayName = providerKey,
  7. Groups = new List<PermissionGroupDto>()
  8. };
  9. var multiTenancySide = CurrentTenant.GetMultiTenancySide();
  10. foreach (var group in await PermissionDefinitionManager.GetGroupsAsync())
  11. {
  12. var groupDto = CreatePermissionGroupDto(group);
  13. var neededCheckPermissions = new List<PermissionDefinition>();
  14. var permissions = group.GetPermissionsWithChildren()
  15. .Where(x => x.IsEnabled)
  16. .Where(x => !x.Providers.Any() || x.Providers.Contains(providerName))
  17. .Where(x => x.MultiTenancySide.HasFlag(multiTenancySide));
  18. foreach (var permission in permissions)
  19. {
  20. if(permission.Parent != null && !neededCheckPermissions.Contains(permission.Parent))
  21. {
  22. continue;
  23. }
  24. if (await SimpleStateCheckerManager.IsEnabledAsync(permission))
  25. {
  26. neededCheckPermissions.Add(permission);
  27. }
  28. }
  29. if (!neededCheckPermissions.Any())
  30. {
  31. continue;
  32. }
  33. var grantInfoDtos = neededCheckPermissions
  34. .Select(CreatePermissionGrantInfoDto)
  35. .ToList();
  36. var multipleGrantInfo = await PermissionManager.GetAsync(neededCheckPermissions.Select(x => x.Name).ToArray(), providerName, providerKey);
  37. foreach (var grantInfo in multipleGrantInfo.Result)
  38. {
  39. var grantInfoDto = grantInfoDtos.First(x => x.Name == grantInfo.Name);
  40. grantInfoDto.IsGranted = grantInfo.IsGranted;
  41. foreach (var provider in grantInfo.Providers)
  42. {
  43. grantInfoDto.GrantedProviders.Add(new ProviderInfoDto
  44. {
  45. ProviderName = provider.Name,
  46. ProviderKey = provider.Key,
  47. });
  48. }
  49. groupDto.Permissions.Add(grantInfoDto);
  50. }
  51. if (groupDto.Permissions.Any())
  52. {
  53. result.Groups.Add(groupDto);
  54. }
  55. }
  56. return result;
  57. }

下面是我们优化后的内容:

  1. public override async Task<GetPermissionListResultDto> GetAsync(string providerName, string providerKey)
  2. {
  3. await CheckProviderPolicy(providerName);
  4. var result = new GetPermissionListResultDto
  5. {
  6. EntityDisplayName = providerKey,
  7. Groups = new List<PermissionGroupDto>()
  8. };
  9. var multiTenancySide = CurrentTenant.GetMultiTenancySide();
  10. // 预先获取所有权限组及其子权限
  11. var groups = await _permissionDefinitionManager.GetGroupsAsync();
  12. // 提前检查所有需要的特性
  13. var featureNames = groups
  14. .Select(group => group.Properties.FirstOrDefault(it => it.Key == "FeatureName").Value?.ToString())
  15. .Where(featureName => !string.IsNullOrEmpty(featureName))
  16. .Distinct()
  17. .ToList();
  18. var enabledFeatures = new HashSet<string>();
  19. foreach (var featureName in featureNames)
  20. {
  21. if (await _featureChecker.IsEnabledAsync(featureName))
  22. {
  23. enabledFeatures.Add(featureName);
  24. }
  25. }
  26. // 批量查询权限状态
  27. var allPermissions = groups.SelectMany(group => group.GetPermissionsWithChildren())
  28. .Where(permission => permission.IsEnabled)
  29. .Where(permission => !permission.Providers.Any() || permission.Providers.Contains(providerName))
  30. .Where(permission => permission.MultiTenancySide.HasFlag(multiTenancySide))
  31. .ToList();
  32. var permissionNames = allPermissions.Select(permission => permission.Name).ToArray();
  33. var multipleGrantInfo = await _permissionManager.GetAsync(permissionNames, providerName, providerKey);
  34. var multipleGrantResult = multipleGrantInfo.Result.ToDictionary(x => x.Name);
  35. // 缓存状态检查结果
  36. var simpleStateCheckerCache = new Dictionary<string, bool>();
  37. foreach (var permission in allPermissions)
  38. {
  39. if (!simpleStateCheckerCache.ContainsKey(permission.Name))
  40. {
  41. simpleStateCheckerCache[permission.Name] = await _simpleStateCheckerManager.IsEnabledAsync(permission);
  42. }
  43. }
  44. }

重构后,耗时不足1s,如下图所示:

权限保存接口优化

针对权限保存的性能问题,我们去除循环操作,一次性读取到内存,在缓冲器进行操作,重构代码如下:

  1. /// <summary>
  2. /// 保存权限
  3. /// </summary>
  4. /// <param name="providerName"></param>
  5. /// <param name="providerKey"></param>
  6. /// <param name="input"></param>
  7. /// <returns></returns>
  8. public async override Task UpdateAsync(string providerName, string providerKey, UpdatePermissionsDto input)
  9. {
  10. await CheckProviderPolicy(providerName);
  11. // 将 Permissions 根据 IsGranted 分为两个列表
  12. var grantedPermissions = input.Permissions
  13. .Where(p => p.IsGranted)
  14. .Select(p => p.Name)
  15. .ToList();
  16. var deniedPermissions = input.Permissions
  17. .Where(p => !p.IsGranted)
  18. .Select(p => p.Name)
  19. .ToList();
  20. // 批量设置 Granted 为 true 的权限
  21. if (grantedPermissions.Any())
  22. {
  23. await _permissionExtendManager.SetBatchAsync(grantedPermissions, providerName, providerKey, isGranted: true);
  24. }
  25. // 批量设置 Granted 为 false 的权限
  26. if (deniedPermissions.Any())
  27. {
  28. await _permissionExtendManager.SetBatchAsync(deniedPermissions, providerName, providerKey, isGranted: false);
  29. }
  30. }

6.死磕代码规范。

一般开源项目不怎么注重代码可读性,abp的模块化设计思想,如果不考虑隔离的代价,其实还是挺不错的,为了项目长期可持续发展,我们还是拟定了适合自己内部的代码规范。

开发规范手册

因为规范是一个团队的标准,老板一般都比较重视,为此我们写了一本开发规范手册,希望对你有所启发。

以上是大的方面,从细节上,我们从DDD的逻辑分层结构入手,每个层、每个类、方法、控制器、api、验证格式、dto、常量等都一一审视一遍。

现摘录一部分:

HttpApi规约

接口数量最佳实践,我们约定:

  • 一个接口能完成的工作,尽量不要用多个接口。
    比如:租户有功能接口,租户版本有功能接口,可以考虑接口进行合并。
  • 多个接口分开做的事情,尽量不要一个接口。
    比如:列表和详情的接口,一般是分开编写的,确保接口的职责单一。
  • 接口分开合并的粒度要适中
    接口什么时候分开,什么时候合并,有个度的问题,比较难把握,如果自己不能确定,需要向上扩大讨论和取舍。坚持质量优先,进度可以适当放慢,不要为了赶进度,不兼顾代码质量,否正代码越多,错误越多,因为代码是需要长期维护的。

接口格式,我们在每个功能开发之前的设计范示:

Domain.Shared规约:

  • 实体文件夹以s结尾;
  • 错误代码以静态类+常量定义在该层,并以ErrorCodes结尾;
  • 全局设置定义在该层,并固定以Settings命名;
  • 多语言的json文件定义在该层;
  • 枚举类和数据库字段长度和默认名称定义在该层。

Domain规约:

  • 实体文件夹约定以s结尾;
  • 表结构字段约定以静态类+常量定义在该层,并以Consts结尾(如果有定义);
  • 全局设置约定定义在该层,并固定以Settings命名(如果有定义);
  • 种子数据约定定义在该层;

EF Core规约:

  • 实体文件夹采用s结尾;
  • 内部包含DbContext,DbContextFactory,ModelCreatingExtensions三个核心类

规范虽好,但是它也是一把双刃剑,开发过程要进行balance,否则会影响团队的效率。比如可以前期起好头,评审频繁一些,后期团队上道了,可以每周审查一次,最好要有审查的标准,而且审核后要督促修改。

再次感谢您的阅读,希望我的文章能成为你成长路上的垫脚石。

原文链接:https://www.cnblogs.com/jackyfei/p/18709265

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号