在 ASP.NET Core 中执行租户服务

本博文翻译自:
http://gunnarpeipman.com/2017/08/tenant-providers/

在我之前关于 Entity Framework core 2.0 全局查询过滤器的文章中,我提出了一个想法,当构建模型时,如何自动地将查询过滤器应用到所有的领域实体中,也就是说领域实体总是来自同一租户。这篇文章更深入地介绍了在 ASP.NET Core 应用程序中检测当前租户的可能解决方案,并建议一些租户提供者将为实际应用程序中提供多租户的支持作为出发点。

注意! 请阅读我之前在Entity Framework core 2.0 全局查询过滤器中的文章,这篇文章将继续下去,并期待读者熟悉我为多租户提供的解决方案。另外,将多租户规则应用到所有领域实体的方法是从我以前的全局查询过滤器中获取的,而不是在这里复制的。

如何检测当前租户?

情况是这样的。数据上下文是在请求传入和构建模型全局查询过滤器时构建的。其中一个过滤器是关于当前租户的。在代码中还需要租户ID,但模型还没有准备好。同一时间,租户ID只能在数据库中使用。我们该怎么办?

一些想法:

  • 在数据上下文中使用数据库连接,并对租户表进行直接查询

  • 为租户的信息和操作使用单独的数据上下文

  • 保持租户信息在云存储上可用

  • 使用域名的哈希值作为租户ID

注意! 在本文中,我希望在web应用程序中通过host的header检测租户。

我在这篇文章中使用的租户表如下图所示。

注意! 依赖于解决方案的租户ID也可以是其他的,而不是像上图所示的int类型。

使用数据上下文连接数据库

这可能是最轻量级的解决方案了,因为不需要添加额外的类,也不再需要租户提供程序。而且使用IHttpContextAccessor很容易获得当前host的header。


public class PlaylistContext : DbContext{   
   private int _tenantId;  
    private string _tenantHost; public DbSet<Playlist> Playlists { get; set; }
    public DbSet<Song> Songs { get; set; }
    public PlaylistContext(DbContextOptions<PlaylistContext> options,                           IHttpContextAccessor accessor)        : base(options)    {_tenantHost = accessor.HttpContext.Request.Host.Value;}
    protected override void OnModelCreating(ModelBuilder modelBuilder)  
   
{      
    var connection = Database.GetDbConnection();   
    using (var command = connection.CreateCommand()){connection.Open();command.CommandText = "select ID from Tenants where Host=@Host";command.CommandType = CommandType.Text; var param = command.CreateParameter();param.ParameterName = "@Host";param.Value = _tenantHost;command.Parameters.Add(param);_tenantId = (int)command.ExecuteScalar();connection.Close();} foreach (var type in GetEntityTypes())        {    
       var method = SetGlobalQueryMethod.MakeGenericMethod(type);method.Invoke(this, new object[] { modelBuilder });} base.OnModelCreating(modelBuilder);} // Other methods follow}

上面的代码是基于数据上下文所持有的数据库连接创建命令,并运行sql命令,以通过host的header来获取租户ID。

这个解决方案的代码量是比较少的,但是它会用主机名检测内部细节的方法来污染数据上下文。

为租户使用单独的数据上下文

第二种方法是使用单独的web应用程序访问特定的租户上下文。可以编写租户提供程序(请参阅我的Entity Framework core 2.0 全局查询过滤器),并将其注入到主数据上下文

让我们从文章开头提到的租户表开始。


public class Tenant{  
   public int Id { get; set; }  
   public string Name { get; set; }  
   public string Host { get; set; } }

现在,让我们构建租户数据上下文。这个上下文不依赖于其他有依赖关系的自定义接口和类。它只使用租户模型。请注意,租户集是私有的,其他类只能通过host的header查询租户ID。


public class TenantsContext : DbContext{  
   private DbSet<Tenant> Tenants { get; set; } public TenantsContext(DbContextOptions<TenantsContext> options)        : base(options)    {}
   protected override void OnModelCreating(ModelBuilder modelBuilder)    {modelBuilder.Entity<Tenant>().HasKey(e => e.Id);}
   public int GetTenantId(string host)    {       var tenant = Tenants.FirstOrDefault(t => t.Host == host);        if(tenant == null){            return 0;} return tenant.Id;} }

现在是时候回到ITenantProvider并编写使用租户数据上下文的实现了。这个提供程序包含检测host的header和获取租户ID的所有逻辑,在实际应用中它将更加复杂,但是在这里我将使用简单的版本。


public class WebTenantProvider : ITenantProvider{ 
   private int _tenantId; public WebTenantProvider(IHttpContextAccessor accessor,                                TenantsContext context)    {  
        var host = accessor.HttpContext.Request.Host.Value;_tenantId = context.GetTenantId(host);} public int GetTenantId()    {  
             return _tenantId;} }

现在,需要检查租户并找到它的ID,因为已经到了重新编写主数据上下文的时候了,所以它使用新的租户提供程序。


public class PlaylistContext : DbContext{   
   private int _tenantId; public DbSet<Playlist> Playlists { get; set; }  
   
   public DbSet<Song> Songs { get; set; }
   public PlaylistContext(DbContextOptions<PlaylistContext> options,                           ITenantProvider tenantProvider)        : base(options)    {_tenantId = tenantProvider.GetTenantId();}
   protected override void OnModelCreating(ModelBuilder modelBuilder)    {        foreach (var type in GetEntityTypes())    
    
{  var method = SetGlobalQueryMethod.MakeGenericMethod(type);method.Invoke(this, new object[] { modelBuilder });} base.OnModelCreating(modelBuilder);} // Other methods follow}

在web应用程序的启动类中,必须在ConfigureServices()方法中 为框架级定义的所有依赖项进行依赖注入。


public void ConfigureServices(IServiceCollection services){services.AddMvc(); var connection = Configuration["ConnectionString"];services.AddEntityFrameworkSqlServer();services.AddDbContext<PlaylistContext>(options => options.UseSqlServer(connection));services.AddDbContext<TenantsContext>(options => options.UseSqlServer(connection));services.AddScoped<ITenantProvider, WebTenantProvider>();services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}

这个解决方案更优雅,因为它将与租户相关的功能从主数据上下文中移出。ITenantProvider是主数据上下文唯一必须知道的东西,现在它也可以在其他不一定是web应用程序的项目中使用。

将租户信息存储在云存储中

我现在说的是,租户并不是一直都在使用,而不是租户提供程序查询数据库,在需要的时候可以缓存租户信息,并在需要时更新它。考虑到云的场景,最好让租户信息在web应用程序的多个实例中都可以访问。我的选择是云存储。

让我们从json格式的简单的租户文件开始,让我们期望它是一些内部应用程序或后台任务的职责,以使这个文件保持最新。这是我使用的样本文件。


[{"Id": 2,"Name": "Local host","Host": "localhost:30172"},{"Id": 3,"Name": "Customer X","Host": "localhost:3331"},{"Id": 4,"Name": "Customer Y","Host": "localhost:33111"}
]

要读取云存储应用程序中的文件,需要了解存储帐户连接字符串、容器名称和云名称。Blob是租户文件。我再次使用ITenantProvider接口,并为Azure 云存储创建了一个新的实现。我把它叫做BlobStorageTenantProvider。它很简单,不需要考虑很多实际的方面,比如刷新租户信息和处理锁。


public class BlobStorageTenantProvider : ITenantProvider{    

private static IList<Tenant> _tenants; private int _tenantId = 0;
public BlobStorageTenantProvider(IHttpContextAccessor accessor, IConfiguration conf)  
 
{      if(_tenants == null){LoadTenants(conf["StorageConnectionString"], conf["TenantsContainerName"], conf["TenantsBlobName"]);} var host = accessor.HttpContext.Request.Host.Value;     
var tenant = _tenants.FirstOrDefault(t => t.Host.ToLower() == host.ToLower());      
if(tenant != null){_tenantId = tenant.Id;}}
private void LoadTenants(string connStr, string containerName, string blobName)    {      
var storageAccount = CloudStorageAccount.Parse(connStr);  
         var blobClient = storageAccount.CreateCloudBlobClient();    
         var container = blobClient.GetContainerReference(containerName);        var blob = container.GetBlobReference(blobName);blob.FetchAttributesAsync().GetAwaiter().GetResult(); var fileBytes = new byte[blob.Properties.Length]; using (var stream = blob.OpenReadAsync().GetAwaiter().GetResult())           using (var textReader = new StreamReader(stream))  
       using (var reader = new JsonTextReader(textReader)){_tenants = JsonSerializer.Create().Deserialize<List<Tenant>>(reader);}}
       public int GetTenantId()    {    
       return _tenantId;} }

提供者的代码可能不是很好,但是它比以前的代码好,因为不需要额外的数据库调用,而且租户id是由内存服务的。

用host的header的哈希值作为租户ID

第三种方法是最简单的方法,但这意味着租户ID与host的 header相同,或者从它派生而来。我不喜欢这种做法,因为如果客户想要更改host的 header,那么更改将分布在整个数据库中。客户可能希望从服务自动提供的自定义主机名开始,然后使用他们自己的子域名。

这里是作为主机名的租户ID的代码。


public class PlaylistContext : DbContext{    

  private
string _tenantId; public DbSet<Playlist> Playlists { get; set; }
   public DbSet<Song> Songs { get; set; }
   public PlaylistContext(DbContextOptions<PlaylistContext> options,                            IHttpContextAccessor accessor)        : base(options)    {_tenantId = accessor.HttpContext.Request.Host.Value;}
   protected override void OnModelCreating(ModelBuilder modelBuilder)    {        foreach (var type in GetEntityTypes())  
     
{ var method = SetGlobalQueryMethod.MakeGenericMethod(type);method.Invoke(this, new object[] { modelBuilder });} base.OnModelCreating(modelBuilder);} // Other methods follow}

可以使用MD5代替主机的名称,但它不会改变主机的问题。

总结

这篇文章是关于在Entity Framework Core 2.0中真正的去利用全局查询过滤器。虽然这里所展示的代码是简单的而不我们实际运用场景所需要的,但在构建真正的解决方案之前,它们仍然是很好的例子。我尽量让解决方案尽可能的接近完美的架构原则。我认为读者他们自己的多租户应用程序可以在这里提供的解决方案中获得帮助。

相关文章: 

  • .NET Core 2.0 正式发布信息汇总

  • .NET Standard 2.0 特性介绍和使用指南

  • .NET Core 2.0 的dll实时更新、https、依赖包变更问题及解决

  • .NET Core 2.0 特性介绍和使用指南

  • Entity Framework Core 2.0 新特性

  • 体验 PHP under .NET Core

  • .NET Core 2.0使用NLog

  • 升级项目到.NET Core 2.0,在Linux上安装Docker,并成功部署

  • 解决Visual Studio For Mac Restore失败的问题

  • ASP.NET Core 2.0 特性介绍和使用指南

  • Entity Framework Core 2.0 全局查询过滤器

  • Entity Framework Core 2.0 特性介绍和使用指南

原文地址:http://www.cnblogs.com/chen-jie/p/tenant-providers.html


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/323919.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

jzoj3512-游戏节目【树状数组,双向dfs】

正题 大意 有n个节目&#xff0c;每个节目对3个东西贡献不同&#xff0c;要求选择至少k个让第一个东西的值最大。求方案数 解题思路 至少k个我们可以计算选择任何个数的结果减去选择k个的结果。由于k比较小&#xff0c;我们考虑直接暴搜 数据不是很大&#xff0c;我们可以将…

【最全最详细】使用publiccms实现动态可维护的首页轮播

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;&#x1f449;雄雄的小课堂&#x1f448;。 &#x1f481;‍♂️前言 前几天&#xff0c;分享了一篇关于publiccms的教程&#xff0c;在这里&#xff1a;【最全最详细】publiccms使用教程&#xff0c;不…

汇编语言(七)之字符串转大写

输入一串字符&#xff0c;将字符串的小写字母转成大写字母 程序运行&#xff1a; 代码&#xff1a; datas segmentoriginalCaseMaxLength db 0ffh,0originalCase db 100h dup(?)uppercase db 100h dup(?)inputPrompt …

基于.NET CORE微服务框架 -谈谈surging API网关

1、前言 对于最近surging更新的API 网关大家也有所关注&#xff0c;也收到了不少反馈提出是否能介绍下Api网关&#xff0c;那么我们将在此篇文章中谈谈surging Api 网关 开源地址&#xff1a;https://github.com/dotnetcore/surging 2. API网关 简介 API 网关是服务提供者…

2018/7/18-纪中某C组题【jzoj3508,jzoj3509,jzoj3510,jzoj3512】

前言 这是比赛的一天后了&#xff0c;第4题调了我超久&#xff0c;其他的都还好。还有LZHdalao给我们讲的第二题超强。 今日分数 Rankperson分数3zyc1253xjq12510蒟蒻9515hjq7515hzb7515lrz7515xxy7525lw15 正题 T1&#xff1a;jzoj3508-好元素【hash,优雅的暴力】 博客链接…

汇编语言(八)之删除数组中为零的元素

删除数组中为零的元素 程序运行&#xff1a; 无输出 代码&#xff1a; datas segmentmem dw 10 dup(0h,34h,0h,56h,32h,10h,3h,13h,0h,0h)memCount dw ($-mem)/2datas endsstacks segment stackdb 100h dup(?)stacks endscodes segmentassume …

【最全最详细】使用publiccms实现动态可维护的导航菜单栏

“大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;????**雄雄的小课堂????。”????‍????前言昨天&#xff0c;给大家整理的是通过publiccms实现动态可维护的轮播图&#xff0c;有需要的小伙伴可以点击这里&#xff1a;publiccms实现动…

golang学习笔记

Panic异常 1、使用defer recover处理 2、或者程序启动时就让他出错 &#xff08;初始化全局变量和 init&#xff09;&#xff08;军哥亲授&#xff09; Json Age int json:"age,omitempty" 序列化时忽略0值或空值type People struct {Name string json:"…

【上海】关于云计算,你想学习哪些知识,快让我来满足你

超高人气、干货十足的 免费云计算课堂 Microsoft Cloud Day云思塾 2017下半年再出发&#xff01; 即将开启上海之旅&#xff0c;微软诚邀您参加&#xff01; Microsoft Cloud Day是个啥&#xff1f; 这是为时一天的结合用户培训与经验分享的云计算免费研讨会&#xff0c;通…

jzoj3461-小麦亩产一千八【斐波那契数列】

正题 大意 第零个格1个&#xff0c;第一格有p个&#xff0c;之后第i格就是第i-1格加i-2格。知道第a格有x个&#xff0c;求第b格有多少个。 解题思路 我们推一下 12345678ppp+1" role="presentation">p+1p+12p12p13p23p25p35p38p58p513p813p821p1321p13不难…

【最全最详细】publiccms实现将公共部分提取成单独模块引入

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;&#x1f449;雄雄的小课堂&#x1f448;。 &#x1f9d8;‍♂️往期系列 这两天一直在整理Publiccms系列的教程&#xff0c;有需要的小伙伴们可以点击以下链接查看&#xff1a; ☝publiccms使用教程&a…

汇编语言(九)之十六进制数值转二进制

输入四位十六进制的数值&#xff0c;将十六进制数值转二进制输出 程序运行&#xff1a; 代码&#xff1a; datas segmentmaxLength db 5hexLength db 0hex db 5 dup(?)bin db 100h dup(?)inputPr…

goland

快捷键 command —ctrl option —alt 文件内搜索&#xff1a; command F ctrl F 前进回退&#xff1a; command option </> ctrl alt </> gofmt&#xff1a; command option L ctrl alt L

.NET Core 2.0 单元测试中初识 IOptionsMonitoramp;lt;Tamp;gt;

在针对下面设置 CookieAuthenticationOptions 的扩展方法写单元测试时遇到了问题。 public static IServiceCollection AddCnblogsAuthentication(this IServiceCollection services, IConfigurationSection redisConfiguration, Action<CookieAuthenticationOptions> …

汇编语言(十)之最小偶数

在数组中查找最小的偶数&#xff0c;并输出 程序运行&#xff1a; 代码&#xff1a; datas segmentminEven dw 0DATA dw 10 dup(101,1,21,81,5,261,3,421,4,541)DATACount dw ($-DATA)/type DATAoutputPrompt db min eve…

jzoj3462-休息【归并排序,逆序对】

正题 大意 一个序列&#xff0c;每次将一个单调下降的区间翻转&#xff0c;求最少次数将这个序列变成单调上升。 解题思路 考虑将O(n2)O(n2)的暴力转换。 我们先将开始时单调下降的区间翻转&#xff0c;然后我们会发现只有每个区间的边界才会需要交换&#xff0c;因为每个区…

【最全最详细】publiccms常用的代码片段

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;&#x1f449;雄雄的小课堂&#x1f448;。 &#x1f9d8;‍♂️往期系列 这两天一直在整理Publiccms系列的教程&#xff0c;有需要的小伙伴们可以点击以下链接查看&#xff1a; ☝publiccms使用教程&a…

Remoting核心类库RealProxy迁移

在学习.net core的过程中&#xff0c;我们已经明确被告知&#xff0c;Remoting将不会被支持。官方的解释是&#xff0c;.net framework 类型包含了太多的Runtime的内容&#xff0c;是一个非常重量级的服务实现&#xff0c;已被确定为一项有问题的体系结构。说白了就是迁移的难度…

【最全最详细】publiccms其他常用代码片段(内容、站点)

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;&#x1f449;雄雄的小课堂&#x1f448;。 ✍往期系列 这两天一直在整理Publiccms系列的教程&#xff0c;有需要的小伙伴们可以点击以下链接查看&#xff1a; ☝publiccms使用教程&#xff08;使用方式…

jzoj3464-秀姿势【hash】

正题 给出n个数&#xff0c;删去k种数&#xff0c;使一种数连续的最长。 解题思路 用hash表储存每种数在leftleft到ii这个区间内没种数的个数,然后如果这个区间内的种数超过k+1个那么就移动left" role="presentation">leftleft指针并没次从hash中弹出一个…