Asp.net core下利用EF core實現從數據實現多租戶(1)


 

前言

隨着互聯網的的高速發展,大多數的公司由於一開始使用的傳統的硬件/軟件架構,導致在業務不斷發展的同時,系統也逐漸地逼近傳統結構的極限。

於是,系統也急需進行結構上的升級換代。

在服務端,系統的I/O是很大的瓶頸。其中數據庫的I/O最容易成為限制系統效率的一環。在優化數據庫I/O這一環中,可以從優化系統調用數據庫效率、數據庫自身效率等多方面入手。

一般情況下,通過升級數據庫服務器的硬件是最容易達到的。但是服務器資源不可能無限擴大,於是從調用數據庫的效率方面入手是目前主流的優化方向。

於是讀寫分離、分庫分表成為了軟件系統的重要一環。並且需要在傳統的系統架構下,是需要做強入侵性的修改。

 

 

什么是多租戶:

多租戶的英文是Multiple Tenancy,很多人把多租戶和Saas划上等號,其實這還是有區別的。我們今天不討論Sass這種如此廣泛的議題。

現在很多的系統都是to B的,它們面向的是組織、公司和商業機構等。每個機構會有獨立的組織架構,獨立的訂單結構,獨立的服務等級和收費。

這就造成了各個機構間的數據是天然獨立的,特別是部分的公司對數據的獨立和安全性會有較高要求,往往數據是需要獨立存儲的。

由於多租戶數據的天然獨立,造成了系統可以根據機構的不同進行分庫分表。所以這里討論的多租戶,僅限於數據層面的!

 

寫這篇文章原因

其實由於一個群的朋友問到了相關的問題,由於當時我並沒有dotnet環境,所以簡單地寫了幾句代碼,我本身是不知道代碼是否正確的。

在我有空的時候,試了一下原來是可實施的。我貼上當時隨手寫的核心代碼,其中connenctionResolver是需要自己創建的。

這代碼是能用的,如果對於asp.net core很熟悉的人,把這段代碼放入到ConfigureServices方法內即可。

但是我還是強烈建議大家跟着我的介紹逐步實施。

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

 

系列文章目錄

主線文章

Asp.net core下利用EF core實現從數據實現多租戶(1) (本文)

Asp.net core下利用EF core實現從數據實現多租戶(2) : 按表分離 

Asp.net core下利用EF core實現從數據實現多租戶(3): 按Schema分離 附加:EF Migration 操作  

 

附加文章

EF core (code first) 通過自定義 Migration History 實現多租戶使用同一數據庫時更新數據庫結構  

EF core (code first) 通過自動遷移實現多租戶數據分離 :按Schema分離數據   

 

實施

項目介紹

這個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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM