zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    Vue无缝滚动
    vue+Axios 实现路由拦截和登录拦截
    添加删除数组元素的方法
    日期时间相关
    Vue源码编译过程
    new关键字执行过程
    预解析
    echarts图表数据为空的时候不显示气泡
    arguments使用
    log4net介绍很全面
  • 原文地址:https://www.cnblogs.com/woailibian/p/12313023.html
Copyright © 2011-2022 走看看