使用EF6简实现多租户的应用
什么是多租户
网上有好多解释,有些上升到了架构设计,让你觉得似乎非常高深莫测,特别是目前流行的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 visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more. 2 public class ApplicationUser : IdentityUser 3 { 4 public async TaskGenerateUserIdentityAsync(UserManager 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 GenerateUserIdentityAsync(UserManager 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 49 { 50 public ApplicationDbContext() 51 : base("DefaultConnection", throwIfV1Schema: false) => Database.SetInitializer (null); 52 53 public static ApplicationDbContext Create() => new ApplicationDbContext(); 54 55 56 }
第二步 修改注册用户的代码,注册新用户的时候需要选择所属的公司信息
1 [HttpPost] 2 [AllowAnonymous] 3 [ValidateAntiForgeryToken] 4 public async TaskRegister(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插入到表中,这里需要引用
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(q => q.Where(x => x.TenantId == tenantid)); 9 //设置当对Order对象进行查询时默认添加过滤条件 10 QueryFilterManager.Filter (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 ()) 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 ()) 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步就实现一个简单的多租户查询数据的功能。
希望对大家有用。