什么是多租户
网上有好多解释,有些上升到了架构设计,让你觉得似乎非常高深莫测,特别是目前流行的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 这个文件
// You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/"http://schemas.microsoft.com/identity/claims/tenantid", this.TenantId.ToString())); userIdentity.AddClaim(new Claim("CompanyName", this.CompanyName)); userIdentity.AddClaim(new Claim("EnabledChat", this.EnabledChat.ToString())); userIdentity.AddClaim(new Claim("FullName", this.FullName)); userIdentity.AddClaim(new Claim("AvatarsX50", this.AvatarsX50)); userIdentity.AddClaim(new Claim("AvatarsX120", this.AvatarsX120)); return userIdentity; } public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager) { // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie); // Add custom user claims here return userIdentity; } [Display(Name = "全名")] public string FullName { get; set; } [Display(Name = "性别")] public int Gender { get; set; } public int AccountType { get; set; } [Display(Name = "所属公司")] public string CompanyCode { get; set; } [Display(Name = "公司名称")] public string CompanyName { get; set; } [Display(Name = "是否在线")] public bool IsOnline { get; set; } [Display(Name = "是否开启聊天功能")] public bool EnabledChat { get; set; } [Display(Name = "小头像")] public string AvatarsX50 { get; set; } [Display(Name = "大头像")] public string AvatarsX120 { get; set; } [Display(Name = "租户ID")] public int TenantId { get; set; } } public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false) => Database.SetInitializer<ApplicationDbContext>(null); public static ApplicationDbContext Create() => new ApplicationDbContext(); }
第二步 修改注册用户的代码,注册新用户的时候需要选择所属的公司信息
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> Register(AccountRegistrationModel viewModel) { var data = this._companyService.Queryable().Select(x => new ListItem() { Value = x.Id.ToString(), Text = x.Name }); this.ViewBag.companylist = data; // Ensure we have a valid viewModel to work with if (!this.ModelState.IsValid) { return this.View(viewModel); } // Try to create a user with the given identity try { // Prepare the identity with the provided information var user = new ApplicationUser { UserName = viewModel.Username, FullName = viewModel.Lastname + "." + viewModel.Firstname, CompanyCode = viewModel.CompanyCode, CompanyName = viewModel.CompanyName, TenantId=viewModel.TenantId, Email = viewModel.Email, AccountType = 0 }; var result = await this.UserManager.CreateAsync(user, viewModel.Password); // If the user could not be created if (!result.Succeeded) { // Add all errors to the page so they can be used to display what went wrong this.AddErrors(result); return this.View(viewModel); } // If the user was able to be created we can sign it in immediately // Note: Consider using the email verification proces await this.SignInAsync(user, true); return this.RedirectToLocal(); } catch (DbEntityValidationException ex) { // Add all errors to the page so they can be used to display what went wrong this.AddErrors(ex); return this.View(viewModel); } } AccountController.cs
第三步 读取登录用户的TenantId 在用户查询和新增修改时把TenantId插入到表中,这里需要引用
Z.EntityFramework.Plus,这个是免费开源的一个类库,功能强大
public StoreContext() : base("Name=DefaultConnection") { //获取登录用户信息,tenantid var claimsidentity = (ClaimsIdentity)HttpContext.Current.User.Identity; var tenantclaim = claimsidentity"http://schemas.microsoft.com/identity/claims/tenantid"); var tenantid = Convert.ToInt32(tenantclaim"http://schemas.microsoft.com/identity/claims/tenantid"); var tenantid = Convert.ToInt32(tenantclaim"LastModifiedDate").IsModified = false; auditableEntity.Property("LastModifiedBy").IsModified = false; auditableEntity.Entity.CreatedDate = currentDateTime; auditableEntity.Entity.CreatedBy = claimsidentity.Name; auditableEntity.Entity.TenantId = tenantid; break; case EntityState.Modified: auditableEntity.Property("CreatedDate").IsModified = false; auditableEntity.Property("CreatedBy").IsModified = false; auditableEntity.Entity.LastModifiedDate = currentDateTime; auditableEntity.Entity.LastModifiedBy = claimsidentity.Name; auditableEntity.Entity.TenantId = tenantid; //if (auditableEntity.Property(p => p.Created).IsModified || auditableEntity.Property(p => p.CreatedBy).IsModified) //{ // throw new DbEntityValidationException(string.Format("Attempt to change created audit trails on a modified {0}", auditableEntity.Entity.GetType().FullName)); //} break; } } } return base.SaveChangesAsync(cancellationToken); } public override int SaveChanges() { var currentDateTime = DateTime.Now; var claimsidentity =(ClaimsIdentity)HttpContext.Current.User.Identity; var tenantclaim = claimsidentity"http://schemas.microsoft.com/identity/claims/tenantid"); var tenantid = Convert.ToInt32(tenantclaim"LastModifiedDate").IsModified = false; auditableEntity.Property("LastModifiedBy").IsModified = false; auditableEntity.Entity.CreatedDate = currentDateTime; auditableEntity.Entity.CreatedBy = claimsidentity.Name; auditableEntity.Entity.TenantId = tenantid; break; case EntityState.Modified: auditableEntity.Property("CreatedDate").IsModified = false; auditableEntity.Property("CreatedBy").IsModified = false; auditableEntity.Entity.LastModifiedDate = currentDateTime; auditableEntity.Entity.LastModifiedBy = claimsidentity.Name; auditableEntity.Entity.TenantId = tenantid; break; } } } return base.SaveChanges(); } DbContext.cs
经过以上3步就实现一个简单的多租户查询数据的功能。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对的支持。
多租户实现,ef6多租户应用
《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线
暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。
艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。
《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。