什么是多租户
网上有好多解释,有些上升到了架构设计,让你觉得似乎非常高深莫测,特别是目前流行的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 这个文件
代码语言:javascript复制 1 // You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://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 }
第二步 修改注册用户的代码,注册新用户的时候需要选择所属的公司信息
代码语言:javascript复制 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 immediately
42 // 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 }
第三步 读取登录用户的TenantId 在用户查询和新增修改时把TenantId插入到表中,这里需要引用
代码语言:javascript复制Z.EntityFramework.Plus,这个是免费开源的一个类库,功能强大
代码语言:javascript复制 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 auditableEntity in 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 }
经过以上3步就实现一个简单的多租户查询数据的功能。
希望对大家有用。