(译)创建.NET Core多租户应用程序-租户解析

介绍

本系列博客文章探讨了如何在ASP.NET Core Web应用程序中实现多租户。这里有很多代码段,因此您可以按照自己的示例应用程序进行操作。在此过程的最后,没有对应的NuGet程序包,但这是一个很好的学习和练习。它涉及到框架的一些“核心”部分。

在本系列的改篇中,我们将解析对租户的请求,并介绍访问该租户信息的能力。

系列目录
  • 第1部分:租户解析(本篇)

  • 第2部分:租户containers

  • 第3部分:每个租户的选项配置

  • 第4部分:每个租户的身份验证

  • 附加:升级到.NET Core 3.1(LTS)

什么是多租户应用程序?

它是一个单一的代码库,根据访问它的“租户”不同而做出不同的响应,您可以使用几种不同的模式,例如

  • 应用程序级别隔离:为每个租户启动一个新网站和相关的依存关系

  • 多租户应用都拥有自己的数据库:租户使用相同的网站,但是拥有自己的数据库

  • 多租户应用程序使用多租户数据库:租户使用相同的网站和相同的数据库(需要注意不要将数据暴露给错误的租户!)

这里有关于每种模式的非常深入的指南。在本系列中,我们将探讨多租户应用程序选项。https://docs.microsoft.com/zh-cn/azure/sql-database/saas-tenancy-app-design-patterns

多租户应用程序需要什么?

多租户应用程序需要满足几个核心要求。

租户解析

从HTTP请求中,我们将需要能够确定在哪个租户上下文中运行请求。这会影响诸如访问哪个数据库或使用哪种配置等问题。

租户应用程序配置

根据加载的租户上下文,可能会对应用程序进行不同的配置,例如OAuth提供程序的身份验证密钥,连接字符串等。

租户数据隔离

租户将需要能够访问他们的数据,以及仅仅访问他们自己的数据。这可以通过在单个数据存储中对数据进行分区或通过使用每个租户的数据存储来实现。无论我们使用哪种模式,我们都应该使开发人员在跨租户场景中难以公开数据以避免编码错误。

租户解析

对于任何多租户应用程序,我们都需要能够识别请求在哪个租户下运行,但是在我们太兴奋之前,我们需要确定查找租户所需的数据。在此阶段,我们实际上只需要一个信息,即租户标识符。

/// <summary>
/// Tenant information
/// </summary>
public class Tenant
{/// <summary>/// The tenant Id/// </summary>public string Id { get; set; }/// <summary>/// The tenant identifier/// </summary>public string Identifier { get; set; }/// <summary>/// Tenant items/// </summary>public Dictionary<string, object> Items { get; private set; } = new Dictionary<string, object>();
}

我们将Identifier根据解析方案策略使用来匹配租户(可能是租户的域名,例如https://{tenant}.myapplication.com)

我们将使用它Id作为对租户的持久引用(Identifier可能会更改,例如主机域更改)。

该属性Items仅用于让开发人员在请求管道期间向租户添加其他内容,如果他们需要特定的属性或方法,他们还可以扩展该类。

常见的租户解决策略

我们将使用解决方案策略将请求匹配到租户,该策略不应依赖任何外部数据来使其变得美观,快速。

主机头

将根据浏览器发送的主机头来推断租户,如果所有租户都具有不同的域(例如)https://host1.example.com,https://host2.example.com或者https://host3.com您支持自定义域,则这是完美的选择。

例如,如果主机标头是,https://host1.example.com我们将Tenant使用Identifier持有值加载host1.example.com。

请求路径

可以根据路线推断租户,例如 https://example.com/host1/...

标头值

可以根据标头值来推断承租人,例如x-tenant: host1,如果所有承租人都可以在核心api上访问,https://api.example.com并且客户端可以指定要与特定标头一起使用的承租人,则这可能很有用。

定义租户解析策略

为了让应用程序知道使用哪种策略,我们应该能够实现ITenantResolutionStrategy将请求解析为租户标识符的服务。

public interface ITenantResolutionStrategy
{Task<string> GetTenantIdentifierAsync();
}

在这篇文章中,我们将实现一个策略,从主机头那里解析租户。

/// <summary>
/// Resolve the host to a tenant identifier
/// </summary>
public class HostResolutionStrategy : ITenantResolutionStrategy
{private readonly IHttpContextAccessor _httpContextAccessor;public HostResolutionStrategy(IHttpContextAccessor httpContextAccessor){_httpContextAccessor = httpContextAccessor;}/// <summary>/// Get the tenant identifier/// </summary>/// <param name="context"></param>/// <returns></returns>public async Task<string> GetTenantIdentifierAsync(){return await Task.FromResult(_httpContextAccessor.HttpContext.Request.Host.Host);}
}
租户存储

现在我们知道要加载哪个租户,该从哪里获取?那将需要某种租户存储。我们将需要实现一个ITenantStore接受承租人标识符并返回Tenant信息的。

public interface ITenantStore<T> where T : Tenant
{Task<T> GetTenantAsync(string identifier);
}

我为什么要使泛型存储?万一我们想在使用我们库的项目中获得更多特定于应用程序的租户信息,我们可以扩展租户使其具有应用程序级别所需的任何其他属性,并适当地配置存储

如果要针对租户存储连接字符串之类的内容,则需要将其放置在安全的地方,并且最好使用每个租户模式的选项配置,并从诸如Azure Key Vault之类的安全地方加载这些字符串。

在这篇文章中,为了简单起见,我们将为租户存储执行一个硬编码的内存中模拟。

/// <summary>
/// In memory store for testing
/// </summary>
public class InMemoryTenantStore : ITenantStore<Tenant>
{/// <summary>/// Get a tenant for a given identifier/// </summary>/// <param name="identifier"></param>/// <returns></returns>public async Task<Tenant> GetTenantAsync(string identifier){var tenant = new[]{new Tenant{ Id = "80fdb3c0-5888-4295-bf40-ebee0e3cd8f3", Identifier = "localhost" }}.SingleOrDefault(t => t.Identifier == identifier);return await Task.FromResult(tenant);}
}
与ASP.NET Core管道集成

有两个主要组成部分

  • 注册你的服务,以便可以解析它们

  • 重新注册一些中间件,以便您可以HttpContext在请求管道中将租户信息添加到当前信息中,从而使下游消费者可以使用它

注册服务

现在,我们有一个获取租户的策略,以及一个使租户脱离的位置,我们需要在应用程序容器中注册这些服务。我们希望该库易于使用,因此我们将使用构建器模式来提供积极的服务注册体验。

首先,我们添加一点扩展以支持.AddMultiTenancy()语法。

/// <summary>
/// Nice method to create the tenant builder
/// </summary>
public static class ServiceCollectionExtensions
{/// <summary>/// Add the services (application specific tenant class)/// </summary>/// <param name="services"></param>/// <returns></returns>public static TenantBuilder<T> AddMultiTenancy<T>(this IServiceCollection services) where T : Tenant=> new TenantBuilder<T>(services);/// <summary>/// Add the services (default tenant class)/// </summary>/// <param name="services"></param>/// <returns></returns>public static TenantBuilder<Tenant> AddMultiTenancy(this IServiceCollection services) => new TenantBuilder<Tenant>(services);
}

然后,我们将让构建器提供“流畅的”扩展。

/// <summary>
/// Configure tenant services
/// </summary>
public class TenantBuilder<T> where T : Tenant
{private readonly IServiceCollection _services;public TenantBuilder(IServiceCollection services){_services = services;}/// <summary>/// Register the tenant resolver implementation/// </summary>/// <typeparam name="V"></typeparam>/// <param name="lifetime"></param>/// <returns></returns>public TenantBuilder<T> WithResolutionStrategy<V>(ServiceLifetime lifetime = ServiceLifetime.Transient) where V : class, ITenantResolutionStrategy{_services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();_services.Add(ServiceDescriptor.Describe(typeof(ITenantResolutionStrategy), typeof(V), lifetime));return this;}/// <summary>/// Register the tenant store implementation/// </summary>/// <typeparam name="V"></typeparam>/// <param name="lifetime"></param>/// <returns></returns>public TenantBuilder<T> WithStore<V>(ServiceLifetime lifetime = ServiceLifetime.Transient) where V : class, ITenantStore<T>{_services.Add(ServiceDescriptor.Describe(typeof(ITenantStore<T>), typeof(V), lifetime));return this;}
}

现在,在.NET Core Web应用程序ConfigureServices中的StartUp类部分中,您可以添加以下内容。

services.AddMultiTenancy().WithResolutionStrategy<HostResolutionStrategy>().WithStore<InMemoryTenantStore>();

这是一个很好的开始但接下来您可能会希望支持传递选项,例如,如果不使用整个域,可能会有一个模式从主机中提取tenantId等,但它现在可以完成任务。

此时,您将能够将存储或解析方案策略注入到控制器中,但这有点低级。您不想在要访问租户的任何地方都必须执行这些解决步骤。接下来,让我们创建一个服务以允许我们访问当前的租户对象。

/// <summary>
/// Tenant access service
/// </summary>
/// <typeparam name="T"></typeparam>
public class TenantAccessService<T> where T : Tenant
{private readonly ITenantResolutionStrategy _tenantResolutionStrategy;private readonly ITenantStore<T> _tenantStore;public TenantAccessService(ITenantResolutionStrategy tenantResolutionStrategy, ITenantStore<T> tenantStore){_tenantResolutionStrategy = tenantResolutionStrategy;_tenantStore = tenantStore;}/// <summary>/// Get the current tenant/// </summary>/// <returns></returns>public async Task<T> GetTenantAsync(){var tenantIdentifier = await _tenantResolutionStrategy.GetTenantIdentifierAsync();return await _tenantStore.GetTenantAsync(tenantIdentifier);}
}

并更新构建器以也注册此服务

public TenantBuilder(IServiceCollection services)
{services.AddTransient<TenantAccessService<T>>();_services = services;
}

酷酷酷酷。现在,您可以通过将服务注入控制器来访问当前租户

/// <summary>
/// A controller that returns a value
/// </summary>
[Route("api/values")]
[ApiController]
public class Values : Controller
{private readonly TenantAccessService<Tenant> _tenantService;/// <summary>/// Constructor with required services/// </summary>/// <param name="tenantService"></param>public Values(TenantAccessService<Tenant> tenantService){_tenantService = tenantService;}/// <summary>/// Get the value/// </summary>/// <param name="definitionId"></param>/// <returns></returns>[HttpGet("")]public async Task<string> GetValue(Guid definitionId){return (await _tenantService.GetTenantAsync()).Id;}
}

运行,您应该会看到根据URL返回的租户ID。

接下来,我们可以添加一些中间件,以将当前的Tenant注入到HttpContext中,这意味着我们可以在可以访问HttpContext的任何地方获取Tenant,从而更加方便。这将意味着我们不再需要大量地注入TenantAccessService。

注册中间件

ASP.NET Core中的中间件使您可以将一些逻辑放入请求处理管道中。在本例中,我们应该在需要访问Tenant信息的任何内容(例如MVC中间件)之前注册中间件。这很可能需要处理请求的控制器中的租户上下文。

首先让我们创建我们的中间件类,这将处理请求并将其注入Tenant当前HttpContext-超级简单。

internal class TenantMiddleware<T> where T : Tenant
{private readonly RequestDelegate next;public TenantMiddleware(RequestDelegate next){this.next = next;}public async Task Invoke(HttpContext context){if (!context.Items.ContainsKey(Constants.HttpContextTenantKey)){var tenantService = context.RequestServices.GetService(typeof(TenantAccessService<T>)) as TenantAccessService<T>;context.Items.Add(Constants.HttpContextTenantKey, await tenantService.GetTenantAsync());}//Continue processingif (next != null)await next(context);}
}

接下来,我们创建一个扩展类使用它。

/// <summary>
/// Nice method to register our middleware
/// </summary>
public static class IApplicationBuilderExtensions
{/// <summary>/// Use the Teanant Middleware to process the request/// </summary>/// <typeparam name="T"></typeparam>/// <param name="builder"></param>/// <returns></returns>public static IApplicationBuilder UseMultiTenancy<T>(this IApplicationBuilder builder) where T : Tenant=> builder.UseMiddleware<TenantMiddleware<T>>();/// <summary>/// Use the Teanant Middleware to process the request/// </summary>/// <typeparam name="T"></typeparam>/// <param name="builder"></param>/// <returns></returns>public static IApplicationBuilder UseMultiTenancy(this IApplicationBuilder builder) => builder.UseMiddleware<TenantMiddleware<Tenant>>();
}

最后,我们可以注册我们的中间件,这样做的最佳位置是在中间件之前,例如MVC可能需要访问Tenant信息的地方。

app.UseMultiTenancy();
app.UseMvc()

现在,Tenant它将位于items集合中,但我们并不是真的要强迫开发人员找出将其存储在哪里,记住类型,需要对其进行转换等。因此,我们将创建一个不错的扩展方法来提取列出当前的租户信息。

/// <summary>
/// Extensions to HttpContext to make multi-tenancy easier to use
/// </summary>
public static class HttpContextExtensions
{/// <summary>/// Returns the current tenant/// </summary>/// <typeparam name="T"></typeparam>/// <param name="context"></param>/// <returns></returns>public static T GetTenant<T>(this HttpContext context) where T : Tenant{if (!context.Items.ContainsKey(Constants.HttpContextTenantKey))return null;return context.Items[Constants.HttpContextTenantKey] as T;}/// <summary>/// Returns the current Tenant/// </summary>/// <param name="context"></param>/// <returns></returns>public static Tenant GetTenant(this HttpContext context){return context.GetTenant<Tenant>();}
}

现在,我们可以修改我们的Values控制器,演示使用当前的HttpContext而不是注入服务。

/// <summary>
/// A controller that returns a value
/// </summary>
[Route("api/values")]
[ApiController]
public class Values : Controller
{/// <summary>/// Get the value/// </summary>/// <param name="definitionId"></param>/// <returns></returns>[HttpGet("")]public async Task<string> GetValue(Guid definitionId){return await Task.FromResult(HttpContext.GetTenant().Id);}
}

如果运行,您将得到相同的结果????

我们的应用程序是“租户感知”的。这是一个重大的里程碑。

‘加个餐’,租户上下文访问者

在ASP.NET Core中,可以使用IHttpContextAccessor访问服务内的HttpContext,为了开发人员提供对租户信息的熟悉访问模式,我们可以创建ITenantAccessor服务。

首先定义一个接口

public interface ITenantAccessor<T> where T : Tenant
{T Tenant { get; }
}

然后实现

public class TenantAccessor<T> : ITenantAccessor<T> where T : Tenant
{private readonly IHttpContextAccessor _httpContextAccessor;public TenantAccessor(IHttpContextAccessor httpContextAccessor){_httpContextAccessor = httpContextAccessor;}public T Tenant => _httpContextAccessor.HttpContext.GetTenant<T>();
}

现在,如果下游开发人员想要向您的应用程序添加一个需要访问当前租户上下文的服务,他们只需以与使用IHttpContextAccessor完全相同的方式注入ITenantAccessor<T>⚡⚡

只需将该TenantAccessService<T>类标记为内部类,这样就不会在我们的程序集之外错误地使用它。

小结

在这篇文章中,我们研究了如何将请求映射到租户。我们将应用程序容器配置为能够解析我们的租户服务,甚至创建了ITenantAccessor服务,以允许在其他服务(如IHttpContextAccessor)内部访问该租赁者。我们还编写了自定义中间件,将当前的租户信息注入到HttpContext中,以便下游中间件可以轻松访问它,并创建了一个不错的扩展方法,以便您可以像HttpContext.GetTenant()一样轻松地获取当前的Tenant。在下一篇文章中,我们将研究按租户隔离数据访问。

在本系列的下一篇文章中,我们将介绍如何在每个租户的基础上配置服务,以便我们可以根据活动的租户解析不同的实现。

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

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

相关文章

LeetCode 404左叶子之和-简单

计算给定二叉树的所有左叶子之和。 示例&#xff1a; 3/ \9 20/ \15 7在这个二叉树中&#xff0c;有两个左叶子&#xff0c;分别是 9 和 15&#xff0c;所以返回 24 代码如下: /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode…

android apt最新版本,解决Android studio 2.3升级到Android studio 3.0 后apt报错问题

解决Android studio 2.3升级到Android studio 3.0 后apt报错问题发布时间&#xff1a;2020-09-16 19:59:42来源&#xff1a;脚本之家阅读&#xff1a;62作者&#xff1a;哈特中尉1.现象描述原来项目在Android studio 2.3一切正常&#xff0c;升级3.0之后报如下错误&#xff1a;…

【要闻】Kubernetes无用论诞生、Elasticsearch 7.6.2 发布

导读&#xff1a;本期要闻包含OpenStack网络如何给组织带来好处、Portworx CEO分享的如何让Kubernetes跑得快还不出错的秘籍等精彩内容。大数据要闻Elasticsearch 7.6.2 发布&#xff0c;分布式搜索和数据分析引擎Elasticsearch 7.6.2 发布了&#xff0c;Elasticsearch 是一个分…

android中弹出窗口,如何在Android中创建弹出窗口(PopupWindow)

如何制作一个简单的Android弹出窗口这是一个更完整的例子。这是一个补充性答案&#xff0c;涉及一般情况下创建弹出窗口的过程&#xff0c;而不一定是OP问题的具体细节。(OP要求取消按钮&#xff0c;但这不是必需的&#xff0c;因为用户可以在屏幕上的任意位置单击以将其取消。…

LeetCode 04检查平衡性-简单

实现一个函数&#xff0c;检查二叉树是否平衡。在这个问题中&#xff0c;平衡树的定义如下&#xff1a;任意一个节点&#xff0c;其两棵子树的高度差不超过 1。 示例 1: 给定二叉树 [3,9,20,null,null,15,7] 3/ \9 20/ \15 7返回 true 。 示例 2: 给定二叉树 [1,2,2,3,…

玩转控件:对Dev中GridControl控件的封装和扩展

清明节清明时节雨纷纷路上行人欲断魂借问酒家何处有牧童遥指杏花村又是一年清明节至&#xff0c;细雨绵绵犹如泪光&#xff0c;树叶随风摆动....转眼间&#xff0c;一年又过去了三分之一&#xff0c;疫情的严峻让不少企业就跟清明时节的树叶一样&#xff0c;摇摇欲坠。裁员的裁…

C++ using namespace 命名空间的定义与使用

#include <iostream> using namespace std;namespace A {int x, y;void fun() {cout << "hello world" << endl;} }int main() {using namespace A;cout << x << endl;fun();return 0; }

android webview framework,android – Webview导致ANR

我编写了一个应用程序,它在Webview中显示html页面,这些页面在ViewPager中管理.一切正常,但是从一页翻到另一页时我有几个ANR. ANR数据转储显示主线程已通过ThreadedRenderer.nSyncAndDrawFrame()调用它调用本机方法,该方法显然与Renderer线程通信,并等待该线程完成绘制视图.由于…

创业5年,我有5点关于人的思考

点击蓝字关注&#xff0c;回复“职场进阶”获取职场进阶精品资料一份不知不觉创业五年了&#xff0c;也算一个屡战屡败、屡败屡战的创业老兵了。从第一次失败要靠吃抗抑郁的药&#xff0c;到现在理性的看待成败得失&#xff0c;不得不说&#xff0c;创业这条路对我还是有不小提…

C++实现具有[数组]相似特征的类DoubleSubscriptArray

#include <iostream> using namespace std;class DoubleSubscriptArray {public:DoubleSubscriptArray(int x, int y) {p new int *[x];//行 //申请行的空间for (int i 0; i < x; i) {p[i] new int [y];//每行的列申请空间}for (int i 0; i < x; i)for (int j …

Docker-HealthCheck指令探测ASP.NET Core容器健康状态

写在前面HealthCheck 不仅是对应用程序内运行情况、数据流通情况进行检查&#xff0c;还包括应用程序对外部服务或依赖资源的健康检查。健康检查通常是以暴露应用程序的HTTP端点的形式实施&#xff0c;可用于配置健康探测的的场景有 &#xff1a;容器或负载均衡器 探测应用状态…

LeetCode28 对称的二叉树-简单

请实现一个函数&#xff0c;用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样&#xff0c;那么它是对称的。 例如&#xff0c;二叉树 [1,2,2,3,4,4,3] 是对称的。 1/ \2 2/ \ / \ 3 4 4 3但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的: 1/ \2 2\ …

ASP.NET Core分布式项目实战(课程介绍,MVP,瀑布与敏捷)--学习笔记

任务1&#xff1a;课程介绍课程目标&#xff1a;1、进一步理解 ASP.NET Core 授权认证框架、MVC 管道2、掌握 Oauth2&#xff0c;结合 Identity Sercer4 实现 OAuth2 和 OpenID Connect Server3、掌握 ASP.NET Core 与 Redis, MongoDB, RabitMQ, MySQL 配合使用4、理解 DDD&…

html坐标轴背景色,CSS 背景(css background)

CSS 背景-CSS background一、Css background背景语法 - TOPCSS背景基础知识CSS 背景这里指通过CSS对对象设置背景属性&#xff0c;如通过CSS设置背景各种样式。背景语法&#xff1a;background: background-color || background-image || background-repeat || background-…

LeetCode 965单值二叉树-简单

如果二叉树每个节点都具有相同的值&#xff0c;那么该二叉树就是单值二叉树。 只有给定的树是单值二叉树时&#xff0c;才返回 true&#xff1b;否则返回 false。 示例 1&#xff1a; 输入&#xff1a;[1,1,1,1,1,null,1] 输出&#xff1a;true 示例 2&#xff1a; 输入&…

使用EF.Core将同一模型映射到多个表

在 EntityFramework Core 中&#xff0c;我们可以使用属性或Fluent API来配置模型映射。有一天&#xff0c;我遇到了一个新的需求&#xff0c;有一个系统每天会生成大量数据&#xff0c;每天生成一个新的表存储数据。例如&#xff0c;数据库如下所示&#xff1a;所有表都具有相…

android创建类的包名称,如何知道/配置Xamarin Android生成的程序包名...

如果您未通过属性指定Xamarin,Xamarin会自动生成类ID一些例子&#xff1a;活动命名&#xff1a;[Activity(Label "ActivityNaming", Name "com.sushihangover.playscript.MyBigBadGame", MainLauncher true, Icon "mipmap/icon")]public cla…

[蓝桥杯2016决赛]愤怒小鸟-模拟

题目描述 X星球愤怒的小鸟喜欢撞火车&#xff01; 一根平直的铁轨上两火车间相距 1000 米。两火车 &#xff08;不妨称A和B&#xff09; 以时速 10米/秒 相对行驶。 愤怒的小鸟从A车出发&#xff0c;时速50米/秒&#xff0c;撞向B车&#xff0c;然后返回去撞A车&#xff0c;再返…

EntityFramework Core 3.x添加查询提示(NOLOCK)

前几天看到有博客园中有园友写了一篇关于添加NOLOCK查询提示的博文&#xff0c;这里呢&#xff0c;我将介绍另外一种添加查询提示的方法&#xff0c;此方式源于我看过源码后的实现&#xff0c;孰好孰歹&#xff0c;请自行判之&#xff0c;接下来我们一起来看看。在EntityFramew…

html自定义鼠标右键,js自定义鼠标右键的实现原理及源码

今天来记录下js来自定义鼠标右键&#xff0c;同样先来分解下它的实现原理&#xff1a;1、屏蔽右键默认事件&#xff1b;(一度我以为修改的就是默认事件)2、对一个ul的隐藏&#xff1b;(这个我也曾迂腐的认为值得这样操作的都是div&#xff0c;汗)3、对鼠标点击右键做出的响应&a…