多租户系统 - 使用 Serenity 服务行为

优质
小牛编辑
125浏览
2023-12-01

如果想把多租户系统扩展到 Northwind 数据库中的其他表,我们会重复角色所做的相同步骤。虽然看起来没那么难,但是有太多的手工工作。

Serenity 提供服务行为系统,它可以允许我们拦截添加、更新、检索、列表、删除的操作处理并向其添加用户自定义代码。

在这些处理中有一些操作(如像获取日志、唯一约束验证等)已经使用服务行为实现了。

行为(Behaviors)可能被所有的行(rows)激活,或被基于某些规则(如特定的特性或接口)的行激活。例如,含 [CaptureLog] 特性的行激活 CaptureLogBehavior。

我们首先定义一个将触发新行为的接口 IMultiTenantRow。把此类放在 TenantRow.cs 旁边的 IMultiTenantRow.cs 中:

  1. using Serenity.Data;
  2. namespace MultiTenancy
  3. {
  4. public interface IMultiTenantRow
  5. {
  6. Int32Field TenantIdField { get; }
  7. }
  8. }

然后在旁边的 MultiTenantBehavior.cs 文件添加行为:

  1. using MultiTenancy.Administration;
  2. using Serenity;
  3. using Serenity.Data;
  4. using Serenity.Services;
  5. namespace MultiTenancy
  6. {
  7. public class MultiTenantBehavior : IImplicitBehavior,
  8. ISaveBehavior, IDeleteBehavior,
  9. IListBehavior, IRetrieveBehavior
  10. {
  11. private Int32Field fldTenantId;
  12. public bool ActivateFor(Row row)
  13. {
  14. var mt = row as IMultiTenantRow;
  15. if (mt == null)
  16. return false;
  17. fldTenantId = mt.TenantIdField;
  18. return true;
  19. }
  20. public void OnPrepareQuery(IRetrieveRequestHandler handler,
  21. SqlQuery query)
  22. {
  23. var user = (UserDefinition)Authorization.UserDefinition;
  24. if (!Authorization.HasPermission(PermissionKeys.Tenants))
  25. query.Where(fldTenantId == user.TenantId);
  26. }
  27. public void OnPrepareQuery(IListRequestHandler handler,
  28. SqlQuery query)
  29. {
  30. var user = (UserDefinition)Authorization.UserDefinition;
  31. if (!Authorization.HasPermission(PermissionKeys.Tenants))
  32. query.Where(fldTenantId == user.TenantId);
  33. }
  34. public void OnSetInternalFields(ISaveRequestHandler handler)
  35. {
  36. if (handler.IsCreate)
  37. fldTenantId[handler.Row] =
  38. ((UserDefinition)Authorization
  39. .UserDefinition).TenantId;
  40. }
  41. public void OnValidateRequest(IDeleteRequestHandler handler)
  42. {
  43. var user = (UserDefinition)Authorization.UserDefinition;
  44. if (fldTenantId[handler.Row] != user.TenantId)
  45. Authorization.ValidatePermission(
  46. PermissionKeys.Tenants);
  47. }
  48. public void OnAfterDelete(IDeleteRequestHandler handler) { }
  49. public void OnAfterExecuteQuery(IRetrieveRequestHandler handler) { }
  50. public void OnAfterExecuteQuery(IListRequestHandler handler) { }
  51. public void OnAfterSave(ISaveRequestHandler handler) { }
  52. public void OnApplyFilters(IListRequestHandler handler, SqlQuery query) { }
  53. public void OnAudit(IDeleteRequestHandler handler) { }
  54. public void OnAudit(ISaveRequestHandler handler) { }
  55. public void OnBeforeDelete(IDeleteRequestHandler handler) { }
  56. public void OnBeforeExecuteQuery(IRetrieveRequestHandler handler) { }
  57. public void OnBeforeExecuteQuery(IListRequestHandler handler) { }
  58. public void OnBeforeSave(ISaveRequestHandler handler) { }
  59. public void OnPrepareQuery(IDeleteRequestHandler handler, SqlQuery query) { }
  60. public void OnPrepareQuery(ISaveRequestHandler handler, SqlQuery query) { }
  61. public void OnReturn(IDeleteRequestHandler handler) { }
  62. public void OnReturn(IRetrieveRequestHandler handler) { }
  63. public void OnReturn(IListRequestHandler handler) { }
  64. public void OnReturn(ISaveRequestHandler handler) { }
  65. public void OnValidateRequest(IRetrieveRequestHandler handler) { }
  66. public void OnValidateRequest(IListRequestHandler handler) { }
  67. public void OnValidateRequest(ISaveRequestHandler handler) { }
  68. }
  69. }

行为类实现 IImplicitBehavior 接口来决定是否应该被指定的行类型(row type)激活。

它是通过实现 ActivateFor 方法做到的,该方法由请求处理(request handlers)调用。

在该方法中,我们检查行类型(row type)是否实现 IMultiTenantRow 接口,如果没有,则返回 false。

然后,我们得到一个 TenantIdField 的私有引用,以便之后在其他方法中使用。

ActivateFor 在每个处理类型(handler type)和行(row)中只被调用一次。如果该方法返回 true,行为实例出于性能考虑而被缓存,并且被该行(row)和处理类型(handler type)重用。

因此,由于每个实例都被所有的请求共享,所以你在其他方法中所写的代码必须是线程安全的。

一个行为通过实现 IRetrieveBehavior, IListBehavior, ISaveBehavior, 或 IDeleteBehavior 接口,可以拦截一个或多个检索列表保存删除 处理。

在这里,我们需要拦截所有这些服务调用,因此我们实现所有的接口。

我们只实现相关的方法,其他的方法保留为空。

我们这里实现的方法,对应于上一章节在 RoleRepository.cs 重写的方法。它们所包含的代码几乎是相同的,但我们这里需要更加通用,因为该行为将为所有实现 IMultiTenantRow 接口的行类型工作。

使用行为重新实现 RoleRepository

现在还原我们在 RoleRepository.cs 做的所有修改:

  1. private class MySaveHandler : SaveRequestHandler<MyRow> { }
  2. private class MyDeleteHandler : DeleteRequestHandler<MyRow> { }
  3. private class MyRetrieveHandler : RetrieveRequestHandler<MyRow> { }
  4. private class MyListHandler : ListRequestHandler<MyRow> { }

并且在 RoleRow 添加 IMultiTenantRow 接口:

  1. namespace MultiTenancy.Administration.Entities
  2. {
  3. //...
  4. public sealed class RoleRow : Row, IIdRow, INameRow, IMultiTenantRow
  5. {
  6. //...
  7. public Int32Field TenantIdField
  8. {
  9. get { return Fields.TenantId; }
  10. }
  11. //...
  12. }
  13. }

使用更少的代码得到相同的结果。声明式编程几乎总是更好的选择。