在 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,一经查实,立即删除!

相关文章

【最全最详细】使用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 网关是服务提供者…

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

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

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

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

【最全最详细】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…

.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…

【最全最详细】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;使用方式…

汇编语言(十一)之统计非数字字符个数

输入以$结尾的字符串&#xff0c;统计并输出非数字字符的个数 程序运行&#xff1a; 代码&#xff1a; datas segmentstring db 100h dup(?)nonNumberCount dw 0inputPrompt db input string and end by $:inputPromptLen …

Orleans配置---持久化

Grain理想的生命周期应该如下图所示: 这就如美国电影中的大反派一样,死了再复活,死了再复活.当然如果复活的反派没有记忆,这个电影就真没劲.现在我要求Grain的复活是有记忆的复活.看我怎么办到. 其中持久化需要用到数据库,因为只有数据库才能比较方便的为以后event sourcing做…

汇编语言(十二)之统计小于平均数的个数

在一个数组中找出并统计并该数组的平均数小的数的个数 程序运行&#xff1a; 代码&#xff1a; datas segmentDATA dw 10h dup(1,2,3,4,-1,5,7,-99,29,-11,294,-21,45,6,57,-54)average dw 0countOfLowAverage dw 0outputAverage …

【最全最详细】publiccmsCSS和JS引入无效的解决方法

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

C#使用Xamarin开发可移植移动应用进阶篇(6.使用渲染器针对单个平台自定义控件),附源码

本篇..基本可以算是Xamarin在应用开发过程中的核心了..真的很很很重要.. 想学习的..想用的..建议仔细阅读..嗯..打酱油的 ..快速滑倒下面点个推荐 - - 哈哈哈... 今天的学习内容? 只讲一个,关于Xamarin.Forms针对各个平台如何进行可定制化的控件操作. 也就是针对某个平台的…

笑脸喜迎新同学,热情送给新伙伴

“大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;????雄雄的小课堂????。”今天&#xff0c;于我和王老师来说&#xff0c;是个非同寻常的一天。那是因为&#xff0c;今天迎来了4班的40余位新同学们&#xff01;一大早&#xff0c;同学们就陆陆…

汇编语言(十三)之偶数转成哥德巴赫猜想

将输入的偶数转换成哥德巴赫猜想&#xff0c;输出哥德巴赫猜想 程序运行&#xff1a; 代码&#xff1a; datas segmentmaxESLen db 0ffhevenSLen db 0evenString db 0ffh dup(?)evenNumber dw ?prime1 dw 0 prime2 dw 0 i…

计算机入门的一些常用小技巧总结

“大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂。”今天总结了一些windows中常用的小技巧&#xff0c;也是明天上课需要演示的&#xff0c;希望对初入计算机行业的你们有所帮助&#xff0c;哈哈哈哈。常用shutdown命令&#xff1a;1.定时…