(译)创建.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,一经查实,立即删除!

相关文章

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

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

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

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

创业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;容器或负载均衡器 探测应用状态…

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;所有表都具有相…

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

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

Xamarin.Forms客户端第一版

1. 功能简介1.1. 读取手机基本信息主要使用Xamarin.Essentials库获取设备基本信息&#xff0c;Xam.Plugin.DeviceInfo插件获取App Id&#xff0c;其实该插件也能获取设备基本信息。1.2. 读取手机联系人信息Android和iOS工程具体实现联系人读取服务&#xff0c;使用到Dependency…

给 EF Core 查询增加 With NoLock

给 EF Core 查询增加 With NoLockIntroEF Core 在 3.x 版本中增加了 Interceptor&#xff0c;使得我们可以在发生低级别数据库操作时作为 EF Core 正常运行的一部分自动调用它们。例如&#xff0c;打开连接、提交事务或执行命令时。所以我们可以自定义一个 Interceptor 来记录执…

LeetCode 138 复制带随机指针的链表-中等

给你一个长度为 n 的链表&#xff0c;每个节点包含一个额外增加的随机指针 random &#xff0c;该指针可以指向链表中的任何节点或空节点。 构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成&#xff0c;其中每个新节点的值都设为其对应的原节点的值。新节点的 n…

ASP.NET Core分布式项目实战(业务介绍,架构设计,oAuth2,IdentityServer4)--学习笔记...

任务4&#xff1a;第一章计划与目录敏捷产品开发流程原型预览与业务介绍整体架构设计API 接口设计 / swaggerIdentity Server 4 搭建登录账号 API 实现配置中心任务5&#xff1a;业务介绍项目背景&#xff1a;基于人脉关系的金融行业项目用户&#xff1a;1、账号&#xff1a;基…

LeetCode 82 删除排序链表中的重复元素||-中等

存在一个按升序排列的链表&#xff0c;给你这个链表的头节点 head &#xff0c;请你删除链表中所有存在数字重复情况的节点&#xff0c;只保留原始链表中 没有重复出现 的数字。 返回同样按升序排列的结果链表。 输入&#xff1a;head [1,2,3,3,4,4,5] 输出&#xff1a;[1,2,…

你复工了吗?啥感受?

这里是Z哥的个人公众号每周五11&#xff1a;45 按时送达当然了&#xff0c;也会时不时加个餐&#xff5e;我的第「136」篇原创敬上感觉还没做什么事情&#xff0c;2020年的第一季度就结束了。相信大多数人也都已经复工了。之前进行远程公办的&#xff0c;大多也都回到了原先在公…

LeetCode 1669合并两个链表-中等

给你两个链表 list1 和 list2 &#xff0c;它们包含的元素分别为 n 个和 m 个。 请你将 list1 中第 a 个节点到第 b 个节点删除&#xff0c;并将list2 接在被删除节点的位置。 下图中蓝色边和节点展示了操作后的结果&#xff1a; 请你返回结果链表的头指针。 输入&#xff1a…

dotNET Core 3.X 使用 Web API

现在的 Web 开发大多都是前后端分离的方式&#xff0c;后端接口的正确使用显得尤为重要&#xff0c;本文讲下在 dotNET Core 3.X 下使用 Web API 。环境操作系统&#xff1a;MacIDE&#xff1a;RiderdotNET Core&#xff1a;3.1创建项目如果是 Windows 操作系统当然是首选 VS20…

你需要了解的 HTTP Status Code

你需要了解的 HTTP Status CodeIntro现在前后端分离的开发模式越来越流行&#xff0c;后端负责开发对应的 API&#xff0c;前端只需要 关注前端页面的数据展示和前端逻辑即可。对于前后端分离这种开发模式&#xff0c;我个人还是比较喜欢的&#xff0c;因为这样可以让更专业的人…

LeetCode 24两两交换链表中的节点-中等

给定一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后的链表。 你不能只是单纯的改变节点内部的值&#xff0c;而是需要实际的进行节点交换。 输入&#xff1a;head [1,2,3,4] 输出&#xff1a;[2,1,4,3] 示例 2&#xff1a; 输入&#xff1a;head []…