Simple implementation of multi tenant application with ef6

Time:2020-5-27

What is multi tenancy

There are many explanations on the Internet. Some of them rise to architecture design, which makes you feel very unpredictable. Especially in the current popular ABP architecture, imusthavetenant is mentioned. In fact, the simple point is to add a tenantid field in each database table to distinguish data belonging to different tenants (or different user groups). The key is that the real way must be transparent to the developers, and they don’t need to pay attention to the information of this field. They can filter and update the data in the background or in the base class.

Fundamentals

When registering from a new user, you must specify the user’s tenantid. My example is to use companyid, company information as tenantid, which users belong to different companies. Each user can only modify and query the data belonging to his company in the future.

The next step is to save the tenantid when the user logs in to obtain the user information, asp.net MVC (not core) is the authentication and authorization implemented by identity 2.0. Here, some code needs to be rewritten.

Finally, when querying / modifying / adding data, users need to set a filter and insert the tenantid of each save change

How to achieve

Step 1, expand Asp.net Identity user property, a tenantid field must be added according to Asp.net Modification of project template provided by MVC IdentityModels.cs This document

// 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.
 public class ApplicationUser : IdentityUser
 {
 public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager, string authenticationType)
 {
  // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
  var userIdentity = await manager.CreateIdentityAsync(this, authenticationType);
  // Add custom user claims here
  userIdentity.AddClaim(new Claim("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 = "full name")]
 public string FullName { get; set; }
 [display (name = "gender")]
 public int Gender { get; set; }
 public int AccountType { get; set; }
 [display (name = "company")]
 public string CompanyCode { get; set; }
 [display (name = "company name")]
 public string CompanyName { get; set; }
 [display (name = "online or not")]
 public bool IsOnline { get; set; }
 [display (name = "enable chat function or not")]
 public bool EnabledChat { get; set; }
 [display (name = "Avatar")]
 public string AvatarsX50 { get; set; }
 [display (name = "big head")]
 public string AvatarsX120 { get; set; }
 [display (name = "tenant 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();


 }

Step 2: modify the code of the registered user. When registering a new user, you need to select the company information


[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

Step 3: read the tenantid of the login user. Insert the tenantid into the table when querying, adding and modifying the user. Here, you need to refer to

Z. EntityFramework.Plus , this is a free and open source class library with powerful functions

public StoreContext()
  : base("Name=DefaultConnection") {
  //Get login user information, tenantid
  var claimsidentity = (ClaimsIdentity)HttpContext.Current.User.Identity;
  var tenantclaim = claimsidentity?.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid");
  var tenantid = Convert.ToInt32(tenantclaim?.Value);
  //Set the default filter conditions to be added when querying work objects
  QueryFilterManager.Filter<Work>(q => q.Where(x => x.TenantId == tenantid));
  //Set the default filter conditions to be added when querying the order object
  QueryFilterManager.Filter<Order>(q => q.Where(x => x.TenantId == tenantid));
 }

 public override Task<int> SaveChangesAsync(CancellationToken cancellationToken)
 {
  var currentDateTime = DateTime.Now;
  var claimsidentity = (ClaimsIdentity)HttpContext.Current.User.Identity;
  var tenantclaim = claimsidentity?.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid");
  var tenantid = Convert.ToInt32(tenantclaim?.Value);
  foreach (var auditableEntity in this.ChangeTracker.Entries<Entity>())
  {
  if (auditableEntity.State == EntityState.Added || auditableEntity.State == EntityState.Modified)
  {
   //auditableEntity.Entity.LastModifiedDate = currentDateTime;
   switch (auditableEntity.State)
   {
   case EntityState.Added:
    auditableEntity.Property("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?.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid");
  var tenantid = Convert.ToInt32(tenantclaim?.Value);
  foreach (var auditableEntity in this.ChangeTracker.Entries<Entity>())
  {
  if (auditableEntity.State == EntityState.Added || auditableEntity.State == EntityState.Modified)
  {
   auditableEntity.Entity.LastModifiedDate = currentDateTime;
   switch (auditableEntity.State)
   {
   case EntityState.Added:
    auditableEntity.Property("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

After the above three steps, a simple multi tenant query data function is realized.

summary

The above is the whole content of this article. I hope that the content of this article has some reference learning value for your study or work. Thank you for your support for developepaer.