Serenity框架官方文档翻译3.2(多租户)

3.2多租户

在本教程中我们将把Norhwind变成一个多租户应用程序。

这是一个维基百科的多租户软件定义:

软件多租户是指一个软件架构的一个实例软件运行在一个服务器和多个租户。租户是一组共享一个公共访问的用户与特定权限的软件实例。多租户架构,软件应用程序旨在提供每个租户专用的实例包括数据、配置、用户管理、租户个体功能和非功能属性。多租户与多实例架构,独立的软件实例操作代表不同的租户。——维基百科

我们将TenantId字段添加到每个表,包括用户,让用户看只到和修改属于她的租户的记录。所以,租户将孤立地工作,就像他们有自己的独立数据库。

多租户应用程序有一些优势,比如减少管理成本。

但是他们也有一些缺点。例如,所有租户数据都在一个单一的数据库,一个租户不能简单地采取单独或备份自己的数据。性能问题是常见的,因为要处理有更多的记录。

云应用程序增加的趋势,虚拟化、成本降低和迁移等功能,它现在更容易设置多实例应用。

我个人避免多租户应用程序。在我看来每客户最好有一个数据库。

但一些用户问到如何实现此功能。本教程将帮助我们解释一些高级的的Serenity主题作为奖励,以及多租户。

你可以在下面找到本教程得源代码

https://github.com/volkanceylan/Serenity-Tutorials/tree/master/MultiTenancy

创建一个新的项目,名称为MultiTenancy

在Visual Studio中单击文件- >新项目。确保你选择Serene 模板,输入MultiTenancy 并单击OK。

在解决方案资源管理器,您应该会看到两个项目 MultiTenancy.Web 和de MultiTenancy.Script.he

确保MultiTenancy.Web 是启动项目(被加粗的),如果不是,右键单击项目名称,然后单击设置为启动项目。

添加项目的依赖

默认情况下,Visual Studio仅仅构建MultiTenancy.Web ,当你按F5运行Web项目。

这是由设置在Visual Studio和解决方案- >选项- >项目构建和运行- >“只构建启动项目和依赖运行”。不建议去改变它。

让脚本项目也建立运行Web项目时,右击 MultiTenancy.Web 项目,单击Build - >项目依赖项并检查多租户的依赖性。脚本依赖选项卡下。

不幸的是没有办法在Serene的模板设置这个依赖。

3.2.1添加租户表和TenantId字段

我们需要给所有表添加一个TenantId字段,来给彼此孤立租户。

因此,我们首先需要一个租户表。

Northwind表已经有记录了,我们将定义一个主要租户ID为1,并将所有现有记录TenantId设置到它。

是时候写一个迁移了,实际上有两个迁移,一个用于Northwind ,一个用于Default数据库。

DefaultDB_20160110_092200_MultiTenant.cs:

using FluentMigrator;

namespace MultiTenancy.Migrations.DefaultDB
{
    [Migration(20160110092200)]
    public class DefaultDB_20160110_092200_MultiTenant 
        : AutoReversingMigration
    {
        public override void Up()
        {
            Create.Table("Tenants")
                .WithColumn("TenantId").AsInt32()
                    .Identity().PrimaryKey().NotNullable()
                .WithColumn("TenantName").AsString(100)
                    .NotNullable();

            Insert.IntoTable("Tenants")
                .Row(new
                {
                    TenantName = "Primary Tenant"
                });

            Insert.IntoTable("Tenants")
                .Row(new
                {
                    TenantName = "Second Tenant"
                });

            Insert.IntoTable("Tenants")
                .Row(new
                {
                    TenantName = "Third Tenant"
                });

            Alter.Table("Users")
                .AddColumn("TenantId").AsInt32()
                    .NotNullable().WithDefaultValue(1);

            Alter.Table("Roles")
                .AddColumn("TenantId").AsInt32()
                    .NotNullable().WithDefaultValue(1);

            Alter.Table("Languages")
                .AddColumn("TenantId").AsInt32()
                    .NotNullable().WithDefaultValue(1);
        }
    }
}

我已经在用户表的默认数据库创建了租户表。这里我们加3个预定义的租户。实际上我们只需要第一个ID为1。

我们不像UserPermissions UserRoles,RolePermissions等添加TenantId列表,因为它们都通过他们的用户id或RoleId 带着TenantId信息。

NorthwindDB_20160110_093500_MultiTenant.cs:

using FluentMigrator;

namespace MultiTenancy.Migrations.NorthwindDB
{
    [Migration(20160110093500)]
    public class NorthwindDB_20160110_093500_MultiTenant 
        : AutoReversingMigration
    {
        public override void Up()
        {
            Alter.Table("Employees")
                .AddColumn("TenantId").AsInt32()
                    .NotNullable().WithDefaultValue(1);

            Alter.Table("Categories")
                .AddColumn("TenantId").AsInt32()
                    .NotNullable().WithDefaultValue(1);

            Alter.Table("Customers")
                .AddColumn("TenantId").AsInt32()
                    .NotNullable().WithDefaultValue(1);

            Alter.Table("Shippers")
                .AddColumn("TenantId").AsInt32()
                    .NotNullable().WithDefaultValue(1);

            Alter.Table("Suppliers")
                .AddColumn("TenantId").AsInt32()
                    .NotNullable().WithDefaultValue(1);

            Alter.Table("Orders")
                .AddColumn("TenantId").AsInt32()
                    .NotNullable().WithDefaultValue(1);

            Alter.Table("Products")
                .AddColumn("TenantId").AsInt32()
                    .NotNullable().WithDefaultValue(1);

            Alter.Table("Region")
                .AddColumn("TenantId").AsInt32()
                    .NotNullable().WithDefaultValue(1);

            Alter.Table("Territories")
                .AddColumn("TenantId").AsInt32()
                    .NotNullable().WithDefaultValue(1);
        }
    }
}

3.2.为Tenants表生成代码

启动Sergen 并且给 Tenants 表生成代码在 Default 连接上:

05591256-60a8-4398-8048-9b81017c1c81

接下来,我们将定义一个查找脚本TenantRow和设置实例名称属性到租户:


namespace MultiTenancy.Administration.Entities
{
    //...
    [ConnectionKey("Default"), DisplayName("Tenants"), 
     InstanceName("Tenant"), TwoLevelCached]
    [LookupScript("Administration.Tenant")]
    public sealed class TenantRow : Row, IIdRow, INameRow
    {
        [DisplayName("Tenant Id"), Identity]
        public Int32? TenantId
        {
            get { return Fields.TenantId[this]; }
            set { Fields.TenantId[this] = value; }
        }

//...

让我们定义一个管理:租户权限,只有admin用户有

namespace MultiTenancy.Administration
{
    public class PermissionKeys
    {
        public const string Security = "Administration:Security";
        public const string Translation = "Administration:Translation";
        public const string Tenants = "Administration:Tenants";
    }
}

在TenantRow上设置:

[ConnectionKey("Default"), DisplayName("Tenants"), InstanceName("Tenant"), TwoLevelCached]
[ReadPermission(PermissionKeys.Tenants)]
[ModifyPermission(PermissionKeys.Tenants)]
[LookupScript("Administration.Tenant")]
public sealed class TenantRow : Row, IIdRow, INameRow
{

3.2.3在用户弹出框选择租户

我们添加一个TenantId字段到 Users 表, 但是它没有在UserRow定义, 所以它在user 弹出框中是不可见的。

这个字段,只能被admin用户编辑何看到。其他用户,即使我们给他们访问页面来管理租户用户,用户不应该能够看到或更改这些信息。

让我们首先添加到UserRow.cs:

namespace MultiTenancy.Administration.Entities
{
    //...
    public sealed class UserRow : LoggingRow, IIdRow, INameRow
    {
        //...
        [DisplayName("Last Directory Update"), Insertable(false), Updatable(false)]
        public DateTime? LastDirectoryUpdate
        {
            get { return Fields.LastDirectoryUpdate[this]; }
            set { Fields.LastDirectoryUpdate[this] = value; }
        }

        [DisplayName("Tenant"), ForeignKey("Tenants", "TenantId"), LeftJoin("tnt")]
        [LookupEditor(typeof(TenantRow))]
        public Int32? TenantId
        {
            get { return Fields.TenantId[this]; }
            set { Fields.TenantId[this] = value; }
        }

        [DisplayName("Tenant"), Expression("tnt.TenantName")]
        public String TenantName
        {
            get { return Fields.TenantName[this]; }
            set { Fields.TenantName[this] = value; }
        }

        //...
        public class RowFields : LoggingRowFields
        {
            //...
            public readonly DateTimeField LastDirectoryUpdate;
            public readonly Int32Field TenantId;
            public readonly StringField TenantName;
            //...
        }
    }
}
要编辑它,我们需要将它添加到UserForm.cs:
namespace MultiTenancy.Administration.Forms
{
    using Serenity;
    using Serenity.ComponentModel;
    using System;
    using System.ComponentModel;

    [FormScript("Administration.User")]
    [BasedOnRow(typeof(Entities.UserRow))]
    public class UserForm
    {
        public String Username { get; set; }
        public String DisplayName { get; set; }
        [EmailEditor]
        public String Email { get; set; }
        [PasswordEditor]
        public String Password { get; set; }
        [PasswordEditor, OneWay]
        public String PasswordConfirm { get; set; }
        [OneWay]
        public string Source { get; set; }
        public Int32? TenantId { get; set; }
    }
}

还需要在site.administration.less增加用户对话框的大小来容下租户选择框。

.s-UserDialog {
    > .size { .widthAndMin(650px); }
    .dialog-styles(@h: auto, @l: 150px, @e: 400px);
    .categories { height: 300px; }
}

现在打开用户管理页面,创建一个用户tenant2属于第二个租户。

9d67880f-4758-416c-a79e-bcfbf161ae58

创建这个用户后,编辑其权限,授予他用户,角色管理和权限许可,因为这将是我们第二个租户的管理用户。

以Tenant2登陆

以用户tenant2 登出和登录。

当你打开用户管理页面,您将看到该用户可以看到和编辑管理用户,除了自己的tenant2用户。他甚至可以在用户对话框查看和编辑租户。

29cf3330-c726-4a35-a3ca-39e6f8ec8728

这不是我们想要的。

让我们阻止他看到其他租户的用户。

3.2.4以TenantId过滤用户

我们首先需要在UserDefinition加载和缓存用户租户信息。

打开Multitenancy.Web/ Modules/ Administration/ User/ Authentication下的 UserDefinition.cs 添加一个TenantId 属性.

namespace MultiTenancy.Administration
{
    using Serenity;
    using System;

    [Serializable]
    public class UserDefinition : IUserDefinition
    {
        public string Id { get { return UserId.ToInvariant(); } }
        public string DisplayName { get; set; }
        public string Email { get; set; }
        public short IsActive { get; set; }
        public int UserId { get; set; }
        public string Username { get; set; }
        public string PasswordHash { get; set; }
        public string PasswordSalt { get; set; }
        public string Source { get; set; }
        public DateTime? UpdateDate { get; set; }
        public DateTime? LastDirectoryUpdate { get; set; }
        public int TenantId { get; set; }
    }
}

当你通过Authorization.UserDefinition请求当前用户,这是当你返回的类。

我们还需要修改代码加载这个类。在同一个文件夹中,编辑UserRetrieveService.cs和改变列表如下:

private UserDefinition GetFirst(IDbConnection connection, BaseCriteria criteria)
{
    var user = connection.TrySingle<Entities.UserRow>(criteria);
    if (user != null)
        return new UserDefinition
        {
            UserId = user.UserId.Value,
            Username = user.Username,
            Email = user.Email,
            DisplayName = user.DisplayName,
            IsActive = user.IsActive.Value,
            Source = user.Source,
            PasswordHash = user.PasswordHash,
            PasswordSalt = user.PasswordSalt,
            UpdateDate = user.UpdateDate,
            LastDirectoryUpdate = user.LastDirectoryUpdate,
            TenantId = user.TenantId.Value
        };

    return null;
}

现在,是时候来通过TenantId过滤用户列表,打开UserRepository.cs,定位MyListHandler类修改:

private class MyListHandler : ListRequestHandler<MyRow>
{
    protected override void ApplyFilters(SqlQuery query)
    {
        base.ApplyFilters(query);

        var user = (UserDefinition)Authorization.UserDefinition;
        if (!Authorization.HasPermission(PermissionKeys.Tenants))
            query.Where(fld.TenantId == user.TenantId);
    }
}

在这里,我们首先获得当前登录用户的用户定义的缓存。

我们检查他是否有租户管理权限,只有管理员才会有。如果没有,我们通过TenantId筛选记录。

3.2.5从用户表单移除 Tenant 下拉框

重建后,启动,现在用户页面将是这样的:

5f2470d8-8928-4fe4-a208-1a15a507fab0

是的,他看不到admin用户了,但是有些错误。当您单击tenant2时,什么都不会发生,你会得到一个错误“无法加载脚本数据:Lookup.Administration.Tenant”:

这个错误与我们最近在仓储层过滤得过滤没有关系。它不能加载这个查找脚本,因为当前用户没有权限租户表。但是他是怎么最后一次看到它?

他能看到它,因为我们第一次登录了管理员和用户,当我们打开编辑对话框加载这个查找脚本。浏览器缓存了它,所以当我们登录tenant2和打开编辑对话框,它从浏览器缓存中加载租户。

但这一次,我们重建项目中,浏览器试图从服务器加载它,我们得到了这个错误,tenant2没有这个权限。没关系,我们不希望他有这个权限,但如何避免这个错误呢?

我们需要从用户表单移除租户字段。但是我们需要该字段为管理员用户,所以我们从UserForm.cs不能简单地删除它。因此,我们需要有条件地这样做。

变换T4所有文件,然后打开UserDialog.cs和重写GetPropertyItems方法如下:

namespace MultiTenancy.Administration
{
    using jQueryApi;
    using Serenity;
    using System.Collections.Generic;
    using System.Linq;

    //...
    public class UserDialog : EntityDialog<UserRow>
    {
        //...
        protected override List<PropertyItem> GetPropertyItems()
        {
            var items = base.GetPropertyItems();

            if (!Authorization.HasPermission("Administration:Tenants"))
                items = items.Where(x => x.Name != UserRow.Fields.TenantId).ToList();

            return items;
        }
    }
}

GetPropertyItems方法,对话框的表单字段的列表,从服务器端表单定义。这些字段从我们定义服务器端UserForm读取。

如果用户没有租户管理权限,我们从客户端表单定义删除TenantId字段。

这并不改变实际的形式定义,只是删除这个对话框的TenantId字段实例。

现在可以自己编辑tenant2用户。

一些用户报告,也要为admin用户删除租户选择。确保你HasPermission方法在MultiTenancy.Script 项目下的Authorization.cs 里面就像下图:

public static bool HasPermission(string permissionKey)
{
  return 
       UserDefinition.Username == "admin" ||
       UserDefinition.Permissions[permissionKey];
}

3.2.6在服务端确保租户选择

当你以tenant2登录用户并且打开他得编辑表单,租户选择下拉不显示,所以他无法改变他的租户对吧?

错!

如果他是一个普通的用户,他不能。但是如果他有一些Serenity 及其服务如何工作的知识,他可以。

当你使用网络,你要认真得多地对待安全。

在web应用程序中很容易创建安全漏洞,除非你在客户端和服务器端处理验证。

让我们展示它。打开浏览器控制台,以用户tenant2登录。

复制这个并粘贴到控制台:

Q.serviceCall({ 
    service: 'Administration/User/Update', 
    request: { 
        EntityId: 2, 
        Entity: { 
            UserId: 2, 
            TenantId: 1 
        }
    }
});
现在刷新用户管理页面,您将看到tenant2现在可以看到admin用户!

我们称为用户更新服务使用javascript,改变tenant2用户TenaNntId 1(主要租户)。

首先让我们恢复它回到第二个租户(2),然后我们会修复这个安全漏洞:

Q.serviceCall({ 
    service: 'Administration/User/Update', 
    request: { 
        EntityId: 2, 
        Entity: { 
            UserId: 2, 
            TenantId: 2 
        }
    }
});

打开UserRepository.cs, 定位到MySaveHandler 类像下面这样修改GetEditableFields方法:

protected override void GetEditableFields(HashSet<Field> editable)
{
    base.GetEditableFields(editable);

    if (!Authorization.HasPermission(Administration.PermissionKeys.Security))
    {
        editable.Remove(fld.Source);
        editable.Remove(fld.IsActive);
    }

    if (!Authorization.HasPermission(Administration.PermissionKeys.Tenants))
    {
        editable.Remove(fld.TenantId);
    }
}

构建您的项目,然后尝试再次输入到控制台:

Q.serviceCall({ 
    service: 'Administration/User/Update', 
    request: { 
        EntityId: 2, 
        Entity: { 
            UserId: 2, 
            TenantId: 1 
        }
    }
});

你将得到这个错误:

Tenant field is read only!

SaveRequestHandler调用GetEditableField 方法来确定那些可更新的用户哪些字段是可编辑的。默认情况下,这些字段是由UpdatableInsertable 的行特性定义的。

除非另有规定,所有字段是可插入的和可更新的。

如果用户没有租户管理权限,我们从auto-determined可编辑字段列表删除TenantId 。

3.2.7为新用户设置TenantId

以Tenant2登录时,试着创建一个新用户,User2。

你不会得到任何错误,而是惊喜,你不会看到新创建的用户列表。User2怎么了?

As we set default value for TenantId to 1 in migrations, now User2 has 1 as TenantId and is a member of Primary Tenant.

当我们在迁移中TenantId设置默认值为1,现在User2有一个 1作为 TenantId和属于主要的租户。

我们必须设置新用户TenantId与登录用户的值相同。

修改UserRepository的SetInternalFields方法,像下面这样:

protected override void SetInternalFields()
{
    base.SetInternalFields();

    if (IsCreate)
    {
        Row.Source = "site";
        Row.IsActive = Row.IsActive ?? 1;
        if (!Authorization.HasPermission(Administration.PermissionKeys.Tenants) ||
            Row.TenantId == null)
        {
            Row.TenantId = ((UserDefinition)Authorization.UserDefinition)
                .TenantId;
        }
    }

    if (IsCreate || !Row.Password.IsEmptyOrNull())
    {
        string salt = null;
        Row.PasswordHash = GenerateHash(password, ref salt);
        Row.PasswordSalt = salt;
    }
}

在这里,我们与当前用户TenantId设置为相同的值,除非他有租户管理权限。

现在尝试创建一个新的用户User2b,这一次你会看到他在名单上。

3.2.8阻止其他租户编辑用户表单

记住用户tenant2可以更新他的TenantId服务调用,而且我们必须确保服务器端。

类似的,即使他在默认情况下从其他租户看不到用户,他可以检索和更新他们。

再次攻击的时间。

打开浏览器控制台输入:

new MultiTenancy.Administration.UserDialog().loadByIdAndOpenDialog(1)

他可以打开admin用户对话框和更新!

当你点击一个username在用户administration 页面,MultiTenancy.Administration.UserDialog是这个对话框类。

我们创建了一个新实例,要求加载用户实体的ID。管理员用户ID为1。

加载ID为1的实体,对话框调用UserRepository的检索服务。

记住我们在UserRepository列表过滤方法,不是检索。服务不知道,从另一个租户,如果它应该返回记录。

是时候在UserRepository安全检索服务:

private class MyRetrieveHandler : RetrieveRequestHandler<MyRow>
{
    protected override void PrepareQuery(SqlQuery query)
    {
        base.PrepareQuery(query);

        var user = (UserDefinition)Authorization.UserDefinition;
        if (!Authorization.HasPermission(PermissionKeys.Tenants))
            query.Where(fld.TenantId == user.TenantId);
    }
}

我们以前在MyListHandler做了同样的改变。

如果你现在尝试相同的Javascript代码,你会得到一个错误:

Record not found. It might be deleted or you don't have required permissions!

But, we could still update record calling Update service manually. So, need to secure MySaveHandler too.

但是,我们仍然可以调用更新服务手动更新记录。因此,也需要确保MySaveHandler。

这样改变其ValidateRequest方法:

protected override void ValidateRequest()
{
    base.ValidateRequest();

    if (IsUpdate)
    {
        var user = (UserDefinition)Authorization.UserDefinition;
        if (Old.TenantId != user.TenantId)
            Authorization.ValidatePermission(PermissionKeys.Tenants);

        // ...

我们检查是否更新,如果TenantId记录被更新(Old.TenantId)是不同于当前登录用户的TenantId。如果是这样,我们调用 Authorization.ValidatePermission方法来确保用户有租户管理的权限。如果没有,它会报错。

Authorization has been denied for this request!

3.2.9防止用户删除其他租户

UserRepository有删除和恢复处理程序,他们遭受类似的安全漏洞。

使用类似的方法,我们需要确保他们:

private class MyDeleteHandler : DeleteRequestHandler<MyRow>
{
    protected override void ValidateRequest()
    {
        base.ValidateRequest();

        var user = (UserDefinition)Authorization.UserDefinition;
        if (Row.TenantId != user.TenantId)
            Authorization.ValidatePermission(PermissionKeys.Tenants);
    }
}

private class MyUndeleteHandler : UndeleteRequestHandler<MyRow>
{
    protected override void ValidateRequest()
    {
        base.ValidateRequest();

        var user = (UserDefinition)Authorization.UserDefinition;
        if (Row.TenantId != user.TenantId)
            Authorization.ValidatePermission(PermissionKeys.Tenants);
    }
}

隐藏租户管理权限

我们现在有一个小问题。用户tenant2有权限Administration:Security ,所以他可以访问用户和角色权限对话框。因此,可以自己在权限UI上给自己授权Administration:Tenants

6872be1a-15dc-4c49-a7b0-5157d2084ec2

Serenity 扫描你的程序集属性像ReadPermission WritePermission,PageAuthorize,ServiceAuthorize等等,在编辑权限对话框中列出这些权限。

 

我们应该先从预填充的列表中删除它。

 

找到方法, ListPermissionKeysUserPermissionRepository.cs中:

public ListResponse<string> ListPermissionKeys()
{
    return LocalCache.Get("Administration:PermissionKeys", TimeSpan.Zero, () =>
    {
        //...

        result.Remove(Administration.PermissionKeys.Tenants);
        result.Remove("*");
        result.Remove("?");

        //...

现在,这个权限不会列在编辑用户权限或编辑角色权限对话框。

但是,尽管如此,他自己可以授予此权限,通过UserPermissionRepository.Update or RolePermissionRepository.Update或者其他得方法黑入。

我们应该添加一些检查来防止这种情况:

public class UserPermissionRepository
{
    public SaveResponse Update(IUnitOfWork uow, 
        UserPermissionUpdateRequest request)
    {
        //...var newList = new Dictionary<string, bool>(
            StringComparer.OrdinalIgnoreCase);
        foreach (var p in request.Permissions)
            newList[p.PermissionKey] = p.Grant ?? false;

        var allowedKeys = ListPermissionKeys()
            .Entities.ToDictionary(x => x);
        if (newList.Keys.Any(x => !allowedKeys.ContainsKey(x)))
            throw new AccessViolationException();
        //...
public class RolePermissionRepository
{
    public SaveResponse Update(IUnitOfWork uow, 
        RolePermissionUpdateRequest request)
    {
        //...var newList = new HashSet<string>(
            request.Permissions.ToList(),
            StringComparer.OrdinalIgnoreCase);

        var allowedKeys = new UserPermissionRepository()
            .ListPermissionKeys()
            .Entities.ToDictionary(x => x);
        if (newList.Any(x => !allowedKeys.ContainsKey(x)))
            throw new AccessViolationException();
        //...

我们检查是否有新的权限试图获得key,不列入许可对话框。

如果是这样,这可能是黑客尝试。

实际上这个检查应该是默认的,即使没有多租户系统,但是通常我们信任管理用户。这里,管理员将只管理自己的租户,所以我们肯定需要这个检查。

3.2.10把角色变成多租户

到目前为止,我们已经把用户页面工作在多租户的风格上。似乎我们做太多的改变使其工作。但请记住,我们正在努力把一个系统变成,而不是被一开始设计成多租户成这样。

我们将相似的原理应用到角色表。

再一次,一个用户不应该看到或修改其他工作区隔离的角色。

We start by adding TenantId property to RoleRow.cs:我们开始通过添加TenantId属性到 RoleRow.cs:

namespace MultiTenancy.Administration.Entities
{
    //...
    public sealed class RoleRow : Row, IIdRow, INameRow
    {
        [Insertable(false), Updatable(false)]
        public Int32? TenantId
        {
            get { return Fields.TenantId[this]; }
            set { Fields.TenantId[this] = value; }
        }

        //...

        public class RowFields : RowFieldsBase
        {
            //...
            public readonly Int32Field TenantId;
            //...
        }
    }
}
然后我们会在RoleRepository.cs做几个变化:
private class MySaveHandler : SaveRequestHandler<MyRow>
{
    protected override void SetInternalFields()
    {
        base.SetInternalFields();

        if (IsCreate)
            Row.TenantId = ((UserDefinition)Authorization.UserDefinition).TenantId;
    }
}

private class MyDeleteHandler : DeleteRequestHandler<MyRow>
{
    protected override void ValidateRequest()
    {
        base.ValidateRequest();

        var user = (UserDefinition)Authorization.UserDefinition;
        if (Row.TenantId != user.TenantId)
            Authorization.ValidatePermission(PermissionKeys.Tenants);
    }
}

private class MyRetrieveHandler : RetrieveRequestHandler<MyRow>
{
    protected override void PrepareQuery(SqlQuery query)
    {
        base.PrepareQuery(query);

        var user = (UserDefinition)Authorization.UserDefinition;
        if (!Authorization.HasPermission(PermissionKeys.Tenants))
            query.Where(fld.TenantId == user.TenantId);
    }
}

private class MyListHandler : ListRequestHandler<MyRow>
{
    protected override void ApplyFilters(SqlQuery query)
    {
        base.ApplyFilters(query);

        var user = (UserDefinition)Authorization.UserDefinition;
        if (!Authorization.HasPermission(PermissionKeys.Tenants))
            query.Where(fld.TenantId == user.TenantId);
    }
}

使用Serenity 的服务行为

如果想在Northwind扩展这个多租户系统到其他表,我们会对角色自行重复相同的步骤。虽然它看起来不那么难,太多的手工工作。Serenity 提供服务的行为系统,你可以拦截创建、更新、检索、列表,删除处理程序和添加自定义代码。

在这些处理程序的一些操作,比如捕获日志,唯一约束验证等已实现为服务行为。行为可能对所有行被激活,或基于一些规则,如有特定属性或接口。

例如,CaptureLogBehavior激活行[CaptureLog]特性。

我们将首先定义一个会触发我们的新行为接口IMultiTenantRow。这个类在文件IMultiTenantRow.cs,TenantRow.cs旁边:

using Serenity.Data;

namespace MultiTenancy
{
    public interface IMultiTenantRow
    {
        Int32Field TenantIdField { get; }
    }
}

然后添加这个behavior在MultiTenantBehavior.cs,旁边

using MultiTenancy.Administration;
using Serenity;
using Serenity.Data;
using Serenity.Services;

namespace MultiTenancy
{
    public class MultiTenantBehavior : IImplicitBehavior,
        ISaveBehavior, IDeleteBehavior,
        IListBehavior, IRetrieveBehavior
    {
        private Int32Field fldTenantId;

        public bool ActivateFor(Row row)
        {
            var mt = row as IMultiTenantRow;
            if (mt == null)
                return false;

            fldTenantId = mt.TenantIdField;
            return true;
        }

        public void OnPrepareQuery(IRetrieveRequestHandler handler, 
            SqlQuery query)
        {
            var user = (UserDefinition)Authorization.UserDefinition;
            if (!Authorization.HasPermission(PermissionKeys.Tenants))
                query.Where(fldTenantId == user.TenantId);
        }

        public void OnPrepareQuery(IListRequestHandler handler, 
            SqlQuery query)
        {
            var user = (UserDefinition)Authorization.UserDefinition;
            if (!Authorization.HasPermission(PermissionKeys.Tenants))
                query.Where(fldTenantId == user.TenantId);
        }

        public void OnSetInternalFields(ISaveRequestHandler handler)
        {
            if (handler.IsCreate)
                fldTenantId[handler.Row] =
                    ((UserDefinition)Authorization
                        .UserDefinition).TenantId;
        }

        public void OnValidateRequest(IDeleteRequestHandler handler)
        {
            var user = (UserDefinition)Authorization.UserDefinition;
            if (fldTenantId[handler.Row] != user.TenantId)
                Authorization.ValidatePermission(
                    PermissionKeys.Tenants);
        }

        public void OnAfterDelete(IDeleteRequestHandler handler) { }
        public void OnAfterExecuteQuery(IRetrieveRequestHandler handler) { }
        public void OnAfterExecuteQuery(IListRequestHandler handler) { }
        public void OnAfterSave(ISaveRequestHandler handler) { }
        public void OnApplyFilters(IListRequestHandler handler, SqlQuery query) { }
        public void OnAudit(IDeleteRequestHandler handler) { }
        public void OnAudit(ISaveRequestHandler handler) { }
        public void OnBeforeDelete(IDeleteRequestHandler handler) { }
        public void OnBeforeExecuteQuery(IRetrieveRequestHandler handler) { }
        public void OnBeforeExecuteQuery(IListRequestHandler handler) { }
        public void OnBeforeSave(ISaveRequestHandler handler) { }       
        public void OnPrepareQuery(IDeleteRequestHandler handler, SqlQuery query) { }
        public void OnPrepareQuery(ISaveRequestHandler handler, SqlQuery query) { }
        public void OnReturn(IDeleteRequestHandler handler) { }
        public void OnReturn(IRetrieveRequestHandler handler) { }
        public void OnReturn(IListRequestHandler handler) { }
        public void OnReturn(ISaveRequestHandler handler) { }
        public void OnValidateRequest(IRetrieveRequestHandler handler) { }
        public void OnValidateRequest(IListRequestHandler handler) { }
        public void OnValidateRequest(ISaveRequestHandler handler) { }
    }
}

行为类IImplicitBehavior接口决定是否应该为特定的行类型被激活。他们这样做通过实现ActivateFor方法,它被请求处理程序调用。在这种方法中,我们检查是否行实现IMultiTenantRow接口类型。如果不是它只是返回false。

然后我们得到一个私有引用到TenantIdField,以后来在后面其他方法重用。

每个处理器类型和行,ActivateFor只调用一次。如果这个方法返回true,由于性能的原因行为实例被缓存,和重用任何请求为这一行和处理类型。

因此,所有你写在其他方法必须是线程安全的,因为一个实例被共享到所有请求。

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

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

我们只填写感兴趣的方法,其他的留空。

我们这里实现的方法,对应于我们覆盖RoleRepository.cs方法。

在前一节。它们包含的代码几乎是相同的,只是在这里我们需要更通用的,因为这种行为将为任何行类型工作实现IMultiTenantRow。

Reimplementing RoleRepository With Using the Behavior

现在我们恢复在RoleRepository.cs里面的更改:

private class MySaveHandler : SaveRequestHandler<MyRow> { }
private class MyDeleteHandler : DeleteRequestHandler<MyRow> { }
private class MyRetrieveHandler : RetrieveRequestHandler<MyRow> { }
private class MyListHandler : ListRequestHandler<MyRow> { }

And add IMultiTenantRow interface to RoleRow:

namespace MultiTenancy.Administration.Entities
{
    //...
    public sealed class RoleRow : Row, IIdRow, INameRow, IMultiTenantRow
    {
        //...
        public Int32Field TenantIdField
        {
            get { return Fields.TenantId; }
        }
        //...
    }
}

你应该用更少的代码得到相同的结果。声明性编程几乎总是更好的。

posted @ 2016-04-02 14:02  zfanlin  阅读(2063)  评论(16编辑  收藏  举报