什么是多租户


网上有好多解释,有些上升到了架构设计,让你觉得似乎非常高深莫测,特别是目前流行的ABP架构中就有提到多租户(IMustHaveTenant),其实说的简单一点就是再每一张数据库的表中添加一个TenantId的字段,用于区分属于不同的租户(或是说不同的用户组)的数据。关键是现实的方式必须对开发人员来说是透明的,不需要关注这个字段的信息,由后台或是封装在基类中实现数据的筛选和更新。

基本原理


从新用户注册时就必须指定用户的TenantId,我的例子是用CompanyId,公司信息做为TenantId,哪些用户属于不同的公司,每个用户将来只能修改和查询属于本公司的数据。

接下来就是用户登录的时候获取用户信息的时候把TenantId保存起来,asp.net mvc(不是 core) 是通过 Identity
2.0实现的认证和授权,这里需要重写部分代码来实现。

最后用户对数据查询/修改/新增时把用户信息中TenantId,这里就需要设定一个Filter(过滤器)和每次SaveChange的插入TenantId

如何实现

第一步,扩展 Asp.net Identity user 属性,必须新增一个TenantId字段,根据Asp.net Mvc
自带的项目模板修改IdentityModels.cs 这个文件
1 // You can add profile data for the user by adding more properties to your
ApplicationUser class, please visithttp://go.microsoft.com/fwlink/?LinkID=317594
to learn more. 2 public class ApplicationUser : IdentityUser 3 { 4 public
async Task<ClaimsIdentity>
GenerateUserIdentityAsync(UserManager<ApplicationUser> manager,string
authenticationType) 5 { 6 // Note the authenticationType must match the one
defined in CookieAuthenticationOptions.AuthenticationType 7 var userIdentity =
await manager.CreateIdentityAsync(this, authenticationType); 8 // Add custom
user claims here 9 userIdentity.AddClaim(new Claim("
http://schemas.microsoft.com/identity/claims/tenantid", this
.TenantId.ToString()));10 userIdentity.AddClaim(new Claim("CompanyName", this
.CompanyName));11 userIdentity.AddClaim(new Claim("EnabledChat", this
.EnabledChat.ToString()));12 userIdentity.AddClaim(new Claim("FullName", this
.FullName));13 userIdentity.AddClaim(new Claim("AvatarsX50", this.AvatarsX50));
14 userIdentity.AddClaim(new Claim("AvatarsX120", this.AvatarsX120)); 15 return
userIdentity;16 } 17 public async Task<ClaimsIdentity>
GenerateUserIdentityAsync(UserManager<ApplicationUser> manager) 18 { 19 //
Note the authenticationType must match the one defined in
CookieAuthenticationOptions.AuthenticationType 20 var userIdentity = await
manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
21 // Add custom user claims here 22 return userIdentity; 23 } 24 25
[Display(Name ="全名")] 26 public string FullName { get; set; } 27 [Display(Name =
"性别")] 28 public int Gender { get; set; } 29 public int AccountType { get; set;
}30 [Display(Name = "所属公司")] 31 public string CompanyCode { get; set; } 32
[Display(Name ="公司名称")] 33 public string CompanyName { get; set; } 34
[Display(Name ="是否在线")] 35 public bool IsOnline { get; set; } 36 [Display(Name =
"是否开启聊天功能")] 37 public bool EnabledChat { get; set; } 38 [Display(Name = "小头像")]
39 public string AvatarsX50 { get; set; } 40 [Display(Name = "大头像")] 41 public
string AvatarsX120 { get; set; } 42 [Display(Name = "租户ID")] 43 public int
TenantId {get; set; } 44 } 45 46 47 48 public class ApplicationDbContext :
IdentityDbContext<ApplicationUser>49 { 50 public ApplicationDbContext() 51 :
base("DefaultConnection", throwIfV1Schema: false) =>
Database.SetInitializer<ApplicationDbContext>(null); 52 53 public static
ApplicationDbContext Create() =>new ApplicationDbContext(); 54 55 56 } View Code
 

  第二步 修改注册用户的代码,注册新用户的时候需要选择所属的公司信息



 

 
1 [HttpPost] 2 [AllowAnonymous] 3 [ValidateAntiForgeryToken] 4 public
async Task<ActionResult> Register(AccountRegistrationModel viewModel) 5 { 6
var data = this._companyService.Queryable().Select(x => new ListItem() { Value
= x.Id.ToString(), Text = x.Name }); 7 this.ViewBag.companylist = data; 8 9
// Ensure we have a valid viewModel to work with 10 if (!this
.ModelState.IsValid)11 { 12 return this.View(viewModel); 13 } 14 15 // Try to
create a user with the given identity 16 try 17 { 18 // Prepare the identity
with the provided information 19 var user = new ApplicationUser 20 { 21
UserName = viewModel.Username, 22 FullName = viewModel.Lastname + "." +
viewModel.Firstname,23 CompanyCode = viewModel.CompanyCode, 24 CompanyName =
viewModel.CompanyName,25 TenantId=viewModel.TenantId, 26 Email =
viewModel.Email,27 AccountType = 0 28 29 }; 30 var result = await this
.UserManager.CreateAsync(user, viewModel.Password);31 32 // If the user could
not be created 33 if (!result.Succeeded) 34 { 35 // Add all errors to the page
so they can be used to display what went wrong 36 this.AddErrors(result); 37 38
return this.View(viewModel); 39 } 40 41 // If the user was able to be created
we can sign it in immediately42 // Note: Consider using the email verification
proces 43 await this.SignInAsync(user, true); 44 45 return this
.RedirectToLocal();46 } 47 catch (DbEntityValidationException ex) 48 { 49 //
Add all errors to the page so they can be used to display what went wrong 50
this.AddErrors(ex); 51 52 return this.View(viewModel); 53 } 54 }
AccountController.cs
第三步 读取登录用户的TenantId 在用户查询和新增修改时把TenantId插入到表中,这里需要引用
Z.EntityFramework.Plus,这个是免费开源的一个类库,功能强大 1 public StoreContext() 2 : base("
Name=DefaultConnection") { 3 //获取登录用户信息,tenantid 4 var claimsidentity =
(ClaimsIdentity)HttpContext.Current.User.Identity; 5 var tenantclaim =
claimsidentity?.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid
"); 6 var tenantid = Convert.ToInt32(tenantclaim?.Value); 7 //
设置当对Work对象进行查询时默认添加过滤条件 8 QueryFilterManager.Filter<Work>(q => q.Where(x =>
x.TenantId == tenantid)); 9 //设置当对Order对象进行查询时默认添加过滤条件 10
QueryFilterManager.Filter<Order>(q => q.Where(x => x.TenantId == tenantid)); 11
}12 13 public override Task<int> SaveChangesAsync(CancellationToken
cancellationToken)14 { 15 var currentDateTime = DateTime.Now; 16 var
claimsidentity = (ClaimsIdentity)HttpContext.Current.User.Identity; 17 var
tenantclaim = claimsidentity?.FindFirst("
http://schemas.microsoft.com/identity/claims/tenantid"); 18 var tenantid =
Convert.ToInt32(tenantclaim?.Value); 19 foreach (var auditableEntity in this
.ChangeTracker.Entries<Entity>()) 20 { 21 if (auditableEntity.State ==
EntityState.Added || auditableEntity.State == EntityState.Modified) 22 { 23 //
auditableEntity.Entity.LastModifiedDate = currentDateTime; 24 switch
(auditableEntity.State)25 { 26 case EntityState.Added: 27
auditableEntity.Property("LastModifiedDate").IsModified = false; 28
auditableEntity.Property("LastModifiedBy").IsModified = false; 29
auditableEntity.Entity.CreatedDate = currentDateTime; 30
auditableEntity.Entity.CreatedBy = claimsidentity.Name; 31
auditableEntity.Entity.TenantId = tenantid; 32 break; 33 case
EntityState.Modified:34 auditableEntity.Property("CreatedDate").IsModified =
false; 35 auditableEntity.Property("CreatedBy").IsModified = false; 36
auditableEntity.Entity.LastModifiedDate = currentDateTime; 37
auditableEntity.Entity.LastModifiedBy = claimsidentity.Name; 38
auditableEntity.Entity.TenantId = tenantid; 39 //if (auditableEntity.Property(p
=> p.Created).IsModified || auditableEntity.Property(p =>
p.CreatedBy).IsModified)40 //{ 41 // throw new
DbEntityValidationException(string.Format("Attempt to change created audit
trails on a modified {0}", auditableEntity.Entity.GetType().FullName));42 //} 43
break; 44 } 45 } 46 } 47 return base.SaveChangesAsync(cancellationToken); 48
}49 50 public override int SaveChanges() 51 { 52 var currentDateTime =
DateTime.Now;53 var claimsidentity =
(ClaimsIdentity)HttpContext.Current.User.Identity;54 var tenantclaim =
claimsidentity?.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid
"); 55 var tenantid = Convert.ToInt32(tenantclaim?.Value); 56 foreach (var
auditableEntityin this.ChangeTracker.Entries<Entity>()) 57 { 58 if
(auditableEntity.State == EntityState.Added || auditableEntity.State ==
EntityState.Modified)59 { 60 auditableEntity.Entity.LastModifiedDate =
currentDateTime;61 switch (auditableEntity.State) 62 { 63 case
EntityState.Added:64 auditableEntity.Property("LastModifiedDate").IsModified =
false; 65 auditableEntity.Property("LastModifiedBy").IsModified = false; 66
auditableEntity.Entity.CreatedDate = currentDateTime; 67
auditableEntity.Entity.CreatedBy = claimsidentity.Name; 68
auditableEntity.Entity.TenantId = tenantid; 69 break; 70 case
EntityState.Modified:71 auditableEntity.Property("CreatedDate").IsModified =
false; 72 auditableEntity.Property("CreatedBy").IsModified = false; 73
auditableEntity.Entity.LastModifiedDate = currentDateTime; 74
auditableEntity.Entity.LastModifiedBy = claimsidentity.Name; 75
auditableEntity.Entity.TenantId = tenantid; 76 break; 77 } 78 } 79 } 80
return base.SaveChanges(); 81 } DbContext.cs
 

经过以上3步就实现一个简单的多租户查询数据的功能。

希望对大家有用。