本博文翻译自:
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跨平台或扫描二维码关注

在 ASP.NET Core 中执行租户服务相关推荐

  1. 如何在ASP.Net Core中使用辅助服务

    ASP.Net Core 3.0 Preview 3添加了对创建工作程序服务的支持,这些工作程序是诸如Windows服务和Linux守护程序之类的后台服务. 还有一个用于在Visual Studio中 ...

  2. 如何在 ASP.NET Core 中为 gRPC 服务添加全局异常处理 ?

    咨询区 Dmitriy 我在 ASP.NET Core 中使用 GRPC.ASPNETCore 工具包写 gRPC 服务,现在我想实现 gRPC 的异常全局拦截,我的代码如下: app.UseExce ...

  3. 一图看懂 ASP.NET Core 中的服务生命周期

    翻译自 Waqas Anwar 2020年11月8日的文章 <ASP.NET Core Service Lifetimes (Infographic)> [1] ASP.NET Core ...

  4. 如何在 ASP.Net Core 中对接 WCF

    在 REST API 出现之前,SOAP (Simple Object Access Protocol) 一直都是基于 web 的标准协议,虽然现在 REST 大行其道,但在平时开发中总会遇到对接第三 ...

  5. asp.net core 中使用 signalR(二)

    asp.net core 使用 signalR(二) Intro 上次介绍了 asp.net core 中使用 signalR 服务端的开发,这次总结一下web前端如何接入和使用 signalR,本文 ...

  6. 【半译】在ASP.NET Core中创建内部使用作用域服务的Quartz.NET宿主服务

    在我的上一篇文章<在ASP.NET Core中创建基于Quartz.NET托管服务轻松实现作业调度>,我展示了如何使用ASP.NET Core创建Quartz.NET托管服务并使用它来按计 ...

  7. ASP.NET Core中的依赖注入(4): 构造函数的选择与服务生命周期管理

    ServiceProvider最终提供的服务实例都是根据对应的ServiceDescriptor创建的,对于一个具体的ServiceDescriptor对象来说,如果它的ImplementationI ...

  8. 如何在 ASP.NET Core 中使用 Quartz.NET 执行任务调度

    当我们在web开发中,常常会遇到这么一个需求,在后台执行某一项具体的任务,具体的说就是这些任务必须在后台定时执行. Quartz.NET 是一个开源的 JAVA 移植版,它有着悠久的历史并且提供了强大 ...

  9. ASP.NET Core中Ocelot的使用:基于服务发现的负载均衡

    本系列相关文章: <ASP.NET Core中Ocelot的使用:API网关的应用> <ASP.NET Core中Ocelot的使用:基于Spring Clound Netflix ...

最新文章

  1. winform 监听http_Winform HttpListener监听有关问题
  2. HashMap数据类型使用注意-不能使用基本数据类型
  3. 美容觉是几点到几点?
  4. pom文件报错,错误如下:(org.apache.maven.project.MavenProject,org.apache.maven.archiver.MavenArchiveConfigurat
  5. Python深度学习:常见优化算法
  6. JAVA- Jersey使用示例
  7. excel拆分单元格内容_Excel中最神奇的一个快捷键!牛!!
  8. 【jpa】简介和项目生成、API-初级入门
  9. 关于NS3中各个WifiRemoteStationManager(二)
  10. [bzoj4698][SDOI2008]Sandy的卡片
  11. 如何1分钟实现身份实名认证功能?
  12. windows10输入法打字没有选字框,【已解决】
  13. FreeBSD_11-系统管理——{Part_3-网络}
  14. ubuntu 命令行更换软件源
  15. 告别LiveServer,vscode微软官方的LivePreview来了
  16. 数据结构与算法之2-3-4树
  17. prism InvokeCommandAction
  18. 达梦DCA课程总结及考试注意点
  19. MySQL中的隐藏列!_rowid
  20. html5程序 pdf,jsPDF – 基于 HTML5 的强大 PDF 生成工具

热门文章

  1. WCF分布式开发常见错误(25):The certificate 'CN=WCFHTTPS' must have a private key
  2. exchange2013 owa-outlook界面语言
  3. Google图片加载库Glide的简单封装GlideUtils
  4. Ubuntu 每日技巧- 自动备份Ubuntu 14.04到Box云存储上
  5. 纯css3实现的鼠标悬停动画按钮
  6. 使用域超级管理员打开Exchange 2010发现没有权限
  7. 使用 FieldMask 提高 C# gRpc 服务性能
  8. 使用 Blazor 开发内部后台(三):登录
  9. .NET 5 自身就是一个 .NET Standard
  10. 在 Xunit 中使用依赖注入