Asp.net core下利用EF core实现从数据实现多租户(1)

 

前言

随着互联网的的高速发展,大多数的公司因为一开始使用的传统的硬件/软件架构,致使在业务不断发展的同时,系统也逐渐地逼近传统结构的极限。git

因而,系统也急需进行结构上的升级换代。github

在服务端,系统的I/O是很大的瓶颈。其中数据库的I/O最容易成为限制系统效率的一环。在优化数据库I/O这一环中,能够从优化系统调用数据库效率、数据库自身效率等多方面入手。sql

通常状况下,经过升级数据库服务器的硬件是最容易达到的。可是服务器资源不可能无限扩大,因而从调用数据库的效率方面入手是目前主流的优化方向。数据库

因而读写分离、分库分表成为了软件系统的重要一环。而且须要在传统的系统架构下,是须要作强入侵性的修改。api

 

 

什么是多租户:

多租户的英文是Multiple Tenancy,不少人把多租户和Saas划上等号,其实这仍是有区别的。咱们今天不讨论Sass这种如此普遍的议题。安全

如今不少的系统都是to B的,它们面向的是组织、公司和商业机构等。每一个机构会有独立的组织架构,独立的订单结构,独立的服务等级和收费。服务器

这就形成了各个机构间的数据是自然独立的,特别是部分的公司对数据的独立和安全性会有较高要求,每每数据是须要独立存储的。架构

因为多租户数据的自然独立,形成了系统能够根据机构的不一样进行分库分表。因此这里讨论的多租户,仅限于数据层面的!app

 

写这篇文章缘由

其实因为一个群的朋友问到了相关的问题,因为当时我并无dotnet环境,因此简单地写了几句代码,我自己是不知道代码是否正确的。asp.net

在我有空的时候,试了一下原来是可实施的。我贴上当时随手写的核心代码,其中connenctionResolver是须要本身建立的。

这代码是能用的,若是对于asp.net core很熟悉的人,把这段代码放入到ConfigureServices方法内便可。

可是我仍是强烈建议你们跟着个人介绍逐步实施。

1 services.AddDbContext<MyContext>((serviceProvider, options)=>
2 {
3     var connenctionResolver = serviceProvider.GetService<IConnectionResolver>();
4     options.UseSqlServer(connenctionResolver.ConnectionString);
5 });

 

 

实施

项目介绍

这个Demo,主要经过根据http request header来获取不一样的租户的标识,从而达到区分租户最终实现数据的隔离。

项目依赖:

1. .net core app 3.1。在机器上安装好.net core SDK, 版本3.1

2. Mysql. 使用 Pomelo.EntityFrameworkCore.MySql 包

3. EF core,Microsoft.EntityFrameworkCore, 版本3.1.1。这里必需要用3.1的,由于ef core3.0是面向.net standard 2.1.  

 

项目中必须对象是什么:

1. DbContext和对应数据库对象

2. ConnenctionResolver, 用于获取链接字符串

3. TenantInfo, 用于表示租户信息

4. TenantInfoMiddleware,用于在asp.net core管道根据http的内容从而解析出TenantInfo

5. Controller, 用于实施对应的

 

实施步骤

1. 建立TenanInfo 和 TenantInfoMiddleware. TenanInfo 做为租户的信息,经过IOC建立,而且在TenantInfoMiddleware经过解析http request header,修改TenantInfo

1 using System;
2 
3 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Infrastructure
4 {
5     public class TenantInfo
6     {
7         public string Name { get; set; }
8     }
9 }
 1 using System;
 2 using System.Threading.Tasks;
 3 using Microsoft.AspNetCore.Http;
 4 using Microsoft.Extensions.DependencyInjection;
 5 
 6 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Infrastructure
 7 {
 8     public class TenantInfoMiddleware
 9     {
10         private readonly RequestDelegate _next;
11 
12         public TenantInfoMiddleware(RequestDelegate next)
13         {
14             _next = next;
15         }
16 
17         public async Task InvokeAsync(HttpContext context)
18         {
19             var tenantInfo = context.RequestServices.GetRequiredService<TenantInfo>();
20             var tenantName = context.Request.Headers["Tenant"];
21 
22             if (string.IsNullOrEmpty(tenantName))
23                 tenantName = "default";
24 
25             tenantInfo.Name = tenantName;
26 
27             // Call the next delegate/middleware in the pipeline
28             await _next(context);
29         }
30     }
31 }
TenantInfoMiddleware

 

2. 建立HttpHeaderSqlConnectionResolver而且实现ISqlConnectionResolver接口。这里要作的事情很简单,直接同TenantInfo取值,而且在配置文件查找对应的connectionString。

其实这个实现类在正常的业务场景是须要包含逻辑的,可是在Demo里为了简明扼要,就使用最简单的方式实现了。

 1 using System;
 2 using Microsoft.Extensions.Configuration;
 3 
 4 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Infrastructure
 5 {
 6     public interface ISqlConnectionResolver
 7     {
 8         string GetConnection();
 9 
10     }
11 
12     public class HttpHeaderSqlConnectionResolver : ISqlConnectionResolver
13     {
14         private readonly TenantInfo tenantInfo;
15         private readonly IConfiguration configuration;
16 
17         public HttpHeaderSqlConnectionResolver(TenantInfo tenantInfo, IConfiguration configuration)
18         {
19             this.tenantInfo = tenantInfo;
20             this.configuration = configuration;
21         }
22         public string GetConnection()
23         {
24             var connectionString = configuration.GetConnectionString(this.tenantInfo.Name);
25             if(string.IsNullOrEmpty(connectionString)){
26                 throw new NullReferenceException("can not find the connection");
27             }
28             return connectionString;
29         }
30     }
31 }
ConnectionResolver

 

3. 建立类MultipleTenancyExtension,里面包含最重要的配置数据库链接字符串的方法。其中里面的DbContext并无使用泛型,是为了更加简明点

 1 using kiwiho.Course.MultipleTenancy.EFcore.Api.DAL;
 2 using Microsoft.Extensions.Configuration;
 3 using Microsoft.Extensions.DependencyInjection;
 4 using Microsoft.EntityFrameworkCore;
 5 
 6 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Infrastructure
 7 {
 8     public static class MultipleTenancyExtension
 9     {
10         public static IServiceCollection AddConnectionByDatabase(this IServiceCollection services)
11         {
12             services.AddDbContext<StoreDbContext>((serviceProvider, options)=>
13             {
14                 var resolver = serviceProvider.GetRequiredService<ISqlConnectionResolver>(); 
15                 
16                 options.UseMySql(resolver.GetConnection());
17             });
18 
19             return services;
20         }
21     }
22 }
MultipleTenancyExtension

 

4. 在Startup类中配置依赖注入和把TenantInfoMiddleware加入到管道中。

1 public void ConfigureServices(IServiceCollection services)
2         {
3             services.AddScoped<TenantInfo>();
4             services.AddScoped<ISqlConnectionResolver, HttpHeaderSqlConnectionResolver>();
5             services.AddConnectionByDatabase();
6             services.AddControllers();
7         }
ConfigureServices

在Configure内,在UseRouting前把TenantInfoMiddleware加入到管道

1 app.UseMiddleware<TenantInfoMiddleware>();

 

5. 配置好DbContext和对应的数据库对象

 1 using Microsoft.EntityFrameworkCore;
 2 
 3 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.DAL
 4 {
 5     public class StoreDbContext : DbContext
 6     {
 7         public DbSet<Product> Products { get; set; }
 8         public StoreDbContext(DbContextOptions options) : base(options)
 9         {
10         }
11     }
12 
13 }
StoreDbContext
 1 using System.ComponentModel.DataAnnotations;
 2 
 3 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.DAL
 4 {
 5     public class Product
 6     {
 7         [Key]
 8         public int Id { get; set; } 
 9 
10         [StringLength(50), Required]
11         public string Name { get; set; }
12 
13         [StringLength(50)]
14         public string Category { get; set; }
15 
16         public double? Price { get; set; }
17     }
18 }
Product

 

6. 建立ProductController, 而且在里面添加3个方法,分别是建立,查询全部,根据id查询。在构造函数经过EnsureCreated以达到在数据库不存在是自动建立数据库。

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Threading.Tasks;
 4 using kiwiho.Course.MultipleTenancy.EFcore.Api.DAL;
 5 using Microsoft.AspNetCore.Mvc;
 6 using Microsoft.EntityFrameworkCore;
 7 
 8 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Controllers
 9 {
10     [ApiController]
11     [Route("api/Products")]
12     public class ProductController : ControllerBase
13     {
14         private readonly StoreDbContext storeDbContext;
15 
16         public ProductController(StoreDbContext storeDbContext){
17             this.storeDbContext = storeDbContext;
18             this.storeDbContext.Database.EnsureCreated();
19         }
20 
21         [HttpPost("")]
22         public async Task<ActionResult<Product>> Create(Product product){
23             var rct = await this.storeDbContext.Products.AddAsync(product);
24 
25             await this.storeDbContext.SaveChangesAsync();
26 
27             return rct?.Entity;
28 
29         }
30 
31         [HttpGet("{id}")]
32         public async Task<ActionResult<Product>> Get([FromRoute] int id){
33 
34             var rct = await this.storeDbContext.Products.FindAsync(id);
35 
36             return rct;
37             
38         }
39 
40         [HttpGet("")]
41         public async Task<ActionResult<List<Product>>> Search(){
42             var rct = await this.storeDbContext.Products.ToListAsync();
43             return rct;
44         }
45     }
46 }
ProductController

 

验证效果

1. 启动项目

 

 2. 经过postman在store1中建立一个Orange,在store2中建立一个cola。要注意的是Headers仲的Tenant:store1是必须的。

图片就只截了store1的例子

 

 3. 分别在store1,store2中查询全部product

store1:只查到了Orange

 

 

store2: 只查到了cola

 

 

4. 经过查询数据库验证数据是否已经隔离。可能有人会以为为何2个id都是1。是由于Product的Id使用 [Key] ,数据库的id是自增加的。

其实这是故意为之的,为的是更好的展现这2个对象是在不一样的数据库

store1:

 

store2:

 

 

 

 

 

总结

这是一个很简单的例子,彷佛把前言读完就已经能实现,那么为何还要花费那么长去介绍呢。

这实际上是一个系列文章,这里只作了最简单的介绍。具体来讲,它真的是一个Demo。

 

接下来要作什么:

在不少实际场景中,其实一个机构一个数据库,这种模式彷佛过重了,并且每一个机构都须要部署数据库服务器和实例好像很难自动化。

而且,大多数的机构,其实彻底没有必要独立一个数据库的。能够经过分表,分Schema实现数据隔离。

因此接下来我会介绍怎么利用EFCore的现有接口实施。而且最终把核心代码作成类库,而且结合MySql,SqlServer作成扩展

 

关于代码

文章中的代码并不是所有代码,若是仅仅拷贝文章的代码可能还不足以实施。可是关键代码已经所有贴出

代码所有放到github上了。这是part1,请checkout分支part1. 或者在master分支上的part1文件夹内。

能够查看master上commit tag是part1 的部分

https://github.com/woailibain/EFCore.MultipleTenancyDemo/tree/part1

相关文章
相关标签/搜索