概要
多租户(Multi Tenancy/Tenant)是一种软件架构,其定义是:在一台服务器上运行单个应用实例,它为多个租户提供服务。
本框架使用的是共享数据库、共享 Schema、共享数据表的数据设计架构。
操作说明
进入系统管理员界面,打开租户管理界面,如下图所示:
下面是租户管理界面:
这里可以管理租户成员,也可以让管理员绑定微信。
下面是公众号配置界面:
这里可以配置公众号的信息。
系统管理员不仅可以管理自己的租户,还可以管理其他租户内容——公众号管理。
下面是公众号管理界面:
架构实现
如上面所述,本框架使用的是共享数据库、共享 Schema、共享数据表的数据设计架构。那么,本框架是如何实现的呢?
主要是分为以下三步:
1. 建立TenantId
2. 扩展ASP.NET Indentity以支持多租户
3. 注册租户筛选器
那么首先,这里需要介绍的是TenantId。
建立租户Id(TenantId)
我们先来看看租户表:
/// <summary> /// 租户信息 /// </summary> public class Account_Tenant { /// <summary> /// 多租户Id /// </summary> public int Id { get; set; } /// <summary> /// 是否为系统租户(仅支持一个) /// </summary> public bool IsSystemTenant { get; set; } /// <summary> /// 租户名称 /// </summary> [Display(Name = "名称")] [Required] [MaxLength(20)] public string Name { get; set; } [Display(Name = "备注")] [DataType(DataType.MultilineText)] [MaxLength(500)] public string Remark { get; set; } }
如上所示,Id为主键,标识列,由数据库自动生成(EF Code First模式下,默认Id为主键,int类型主键自动设置为标识列)。
那么,租户Id产生了之后,所有租户共享数据表存放数据,不同租户的数据需要通过 TenantId 字段来区分。
我们来看一个基类的设计:
public abstract class WeiChat_TenantBase<TKey> : ITenantId, IAdminCreate<string>, IAdminUpdate<string> { [Key] public virtual TKey Id { get; set; } /// <summary> /// 创建时间 /// </summary> [Display(Name = "创建时间")] public DateTime CreateTime { get; set; } /// <summary> /// 更新时间 /// </summary> [Display(Name = "更新时间")] public DateTime? UpdateTime { get; set; } /// <summary> /// 创建者 /// </summary> [MaxLength(128)] public string CreateBy { get; set; } /// <summary> /// 创建者 /// </summary> [Display(Name = "创建者")] //[NotMapped] [ForeignKey("CreateBy")] public AppUser CreateUser { get; set; } /// <summary> /// 更新者 /// </summary> [MaxLength(128)] public string UpdateBy { get; set; } /// <summary> /// 编辑者 /// </summary> [MaxLength(256)] [Display(Name = "最后编辑")] //[NotMapped] public AppUser UpdateUser { get; set; } public int TenantId { get; set; } }
如上所示,TenantId就是数据的分水岭,不同数据的筛选需要根据其来筛选。
如下图的设计:
从上图可以看出,这块错综复杂的类都缺不了TenantId,可能看类还是不太明白,我们来看表结构吧,比如说:
等等。如上面表结构所示,TenantId为个表间必备字段。
而在Code First模式下,使用继承可以很方便的将所有的模型类加上相关字段。
众所周知,本框架使用了ASP.NET Indentity,那么如何对ASP.NET Indentity实现多租户的扩展呢?
扩展ASP.NET Indentity以支持多租户
在本框架中,编写了库Magicodes.WeiChat.Data.Multitenant,用于扩展ASP.NET Indentity以支持多租户。
使用过ASP.NET Indentity的朋友应该都知道Microsoft.AspNet.Identity.EntityFramework——ASP.NET Indentity使用EF作为其数据存储的实现库。通过对象浏览器查看,不难看出,其主要定义了以下对象:
其中,IdentityDbContext 继承自System.Data.Entity.DbContext,具体定义如下所示:
public class IdentityDbContext<TUser, TRole, TKey, TUserLogin, TUserRole, TUserClaim> : System.Data.Entity.DbContext
where TUser : Microsoft.AspNet.Identity.EntityFramework.IdentityUser<TKey, TUserLogin, TUserRole, TUserClaim>
where TRole : Microsoft.AspNet.Identity.EntityFramework.IdentityRole<TKey, TUserRole>
where TUserLogin : Microsoft.AspNet.Identity.EntityFramework.IdentityUserLogin<TKey>
where TUserRole : Microsoft.AspNet.Identity.EntityFramework.IdentityUserRole<TKey>
where TUserClaim : Microsoft.AspNet.Identity.EntityFramework.IdentityUserClaim<TKey>
Microsoft.AspNet.Identity.EntityFramework 的成员
那么,我们现在的主要对象就是搞定她们了。
一一对应关系如下所示:
如上所示,通过扩展ASP.NET Identity的IUser、IdentityUser、IdentityDbContext、IdentityUserLogin、UserStore来完成了对多租户的支持,同时需要注意的是,还要重写方法FindByNameAsync、AddLoginAsync、FindAsync、FindByEmailAsync、CreateAsync,以支持多租户。
完成了对ASP.NET Identity的多租户的支持,我们还需要对数据进行筛选,但是所有地方都添加筛选代码是一件很麻烦的事情,而且在编写逻辑的时候还很容易健忘,那么有什么好的方式呢?是时候祭出我们的神器了——EntityFramework.DynamicFilters。
注册租户筛选器
筛选器依赖ENTITYFRAMEWORK.DYNAMICFILTERS,这是一个开源项目,相关介绍可以访问以下链接:
https://github.com/jcachat/EntityFramework.DynamicFilters
这里,我们定义了如下租户筛选器:
modelBuilder.Filter("TenantEntryFilter", (ITenantId app, int tenantId) => (app.TenantId == tenantId), 0);
然后我们可以使用以下代码来启用筛选器:
db.EnableFilter(tenantFilterName);
//设置多租户过滤
db.SetFilterScopedParameterValue(tenantFilterName, "tenantId", TenantId);
以上代码大家可以写到通用的地方进行封装,比如控制器基类的OnActionExecuting方法中。
尾声
至此,整个多租户的架构就基本完成了。当然我们还可以进行扩展,比如实现租户缓存、租户资源管理等等,这是后续的话题了。