ASP.NET Core + SaasKit + PostgreSQL + Citus 的多租户应用程序架构示例

虚幻大学 xuhss 137℃ 0评论

? 优质资源分享 ?

学习路线指引(点击解锁) 知识定位 人群定位
? Python实战微信订餐小程序 ? 进阶级 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
?Python量化交易实战? 入门级 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统

19311961ee10a03c730dba059e0f0d35 - ASP.NET Core + SaasKit + PostgreSQL + Citus 的多租户应用程序架构示例

确定分布策略 中,
我们讨论了在多租户用例中使用 Citus 所需的与框架无关的数据库更改。
当前部分研究如何构建与 Citus 存储后端一起使用的多租户 ASP.NET 应用程序。

示例应用

为了使这个迁移部分具体化,
让我们考虑一个简化版本的 StackExchange。
供参考,最终结果存在于 Github 上。

Schema

我们将从两张表开始:


    CREATE TABLE tenants (
        id uuid NOT NULL,
        domain text NOT NULL,
        name text NOT NULL,
        description text NOT NULL,
        created_at timestamptz NOT NULL,
        updated_at timestamptz NOT NULL
    );

    CREATE TABLE questions (
        id uuid NOT NULL,
        tenant_id uuid NOT NULL,
        title text NOT NULL,
        votes int NOT NULL,
        created_at timestamptz NOT NULL,
        updated_at timestamptz NOT NULL
    );

    ALTER TABLE tenants ADD PRIMARY KEY (id);
    ALTER TABLE questions ADD PRIMARY KEY (id, tenant_id);

我们 demo 应用程序的每个租户都将通过不同的域名进行连接。
ASP.NET Core 将检查传入请求并在 tenants 表中查找域。
您还可以按子域(或您想要的任何其他 scheme)查找租户。

注意 tenant_id 是如何存储在 questions 表中的。
这将使 :ref:colocate 数据成为可能。
创建表后,使用 create_distributed table 告诉 Citus 对租户 ID 进行分片:


    SELECT create_distributed_table('tenants', 'id');
    SELECT create_distributed_table('questions', 'tenant\_id');

接下来包括一些测试数据。


    INSERT INTO tenants VALUES (
        'c620f7ec-6b49-41e0-9913-08cfe81199af', 
        'bufferoverflow.local',
        'Buffer Overflow',
        'Ask anything code-related!',
        now(),
        now());

    INSERT INTO tenants VALUES (
        'b8a83a82-bb41-4bb3-bfaa-e923faab2ca4', 
        'dboverflow.local',
        'Database Questions',
        'Figure out why your connection string is broken.',
        now(),
        now());

    INSERT INTO questions VALUES (
        '347b7041-b421-4dc9-9e10-c64b8847fedf',
        'c620f7ec-6b49-41e0-9913-08cfe81199af',
        'How do you build apps in ASP.NET Core?',
        1,
        now(),
        now());

    INSERT INTO questions VALUES (
        'a47ffcd2-635a-496e-8c65-c1cab53702a7',
        'b8a83a82-bb41-4bb3-bfaa-e923faab2ca4',
        'Using postgresql for multitenant data?',
        2,
        now(),
        now());

这样就完成了数据库结构和示例数据。 我们现在可以继续设置 ASP.NET Core。

ASP.NET Core 项目

如果您没有安装 ASP.NET Core,请安装 Microsoft 的 .NET Core SDK
这些说明将使用 dotnet CLI,
但如果您使用的是 Windows,
也可以使用 Visual Studio 2017 或更高版本。

使用 dotnet new 从 MVC 模板创建一个新项目:

dotnet new mvc -o QuestionExchange
cd QuestionExchange

如果您愿意,可以使用 dotnet run 预览模板站点。
MVC 模板几乎包含您开始使用的所有内容,但 Postgres 支持并不是开箱即用的。
你可以通过安装 Npgsql.EntityFrameworkCore.PostgreSQL 包来解决这个问题:

dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL

此包将 Postgres 支持添加到 Entity Framework Core、ASP.NET Core 中的默认 ORM 和数据库层。
打开 Startup.cs 文件并将这些行添加到 ConfigureServices 方法的任意位置:

var connectionString = "connection-string";

services.AddEntityFrameworkNpgsql()
    .AddDbContext(options => options.UseNpgsql(connectionString));

您还需要在文件顶部添加这些声明:

using Microsoft.EntityFrameworkCore;
using QuestionExchange.Models;

connection-string 替换为您的 Citus 连接字符串。我的看起来像这样:

Server=myformation.db.citusdata.com;Port=5432;Database=citus;Userid=citus;Password=mypassword;SslMode=Require;Trust Server Certificate=true;

您可以使用 Secret
Manager
来避免将数据库凭据存储在代码中(并意外将它们检入源代码控制中)。

接下来,您需要定义一个数据库上下文。

添加 Tenancy(租赁) 到 App

定义 Entity Framework Core 上下文和模型

数据库上下文类提供代码和数据库之间的接口。
Entity Framework Core 使用它来了解您的 data
schema
是什么样的,
因此您需要定义数据库中可用的表。

在项目根目录中创建一个名为 AppDbContext.cs 的文件,并添加以下代码:

using System.Linq;
using Microsoft.EntityFrameworkCore;
using QuestionExchange.Models;
namespace QuestionExchange
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions options)
            : base(options)
        {
        }

        public DbSet Tenants { get; set; }

 public DbSet Questions { get; set; }
 }
}

两个 DbSet 属性指定用于对每个表的行建模的 C# 类。
接下来您将创建这些类。在此之前,请在 Questions 属性下方添加一个新方法:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var mapper = new Npgsql.NpgsqlSnakeCaseNameTranslator();
    var types = modelBuilder.Model.GetEntityTypes().ToList();

    // Refer to tables in snake\_case internally
    types.ForEach(e => e.Relational().TableName = mapper.TranslateMemberName(e.Relational().TableName));

    // Refer to columns in snake\_case internally
    types.SelectMany(e => e.GetProperties())
        .ToList()
        .ForEach(p => p.Relational().ColumnName = mapper.TranslateMemberName(p.Relational().ColumnName));
}

C# 类和属性按惯例是 PascalCase,但 Postgres 表和列是小写的(和 snake_case)。
OnModelCreating 方法允许您覆盖默认名称转换并让 Entity Framework Core 知道如何在数据库中查找实体。

现在您可以添加代表租户和问题的类。
在 Models 目录中创建一个 Tenant.cs 文件:

using System;

namespace QuestionExchange.Models
{
    public class Tenant
 {
        public Guid Id { get; set; }

        public string Domain { get; set; }

        public string Name { get; set; }

        public string Description { get; set; }

        public DateTimeOffset CreatedAt { get; set; }

        public DateTimeOffset UpdatedAt { get; set; }
    }
}

还有一个 Question.cs 文件,也在 Models 目录中:

using System;

namespace QuestionExchange.Models
{
    public class Question
 {
        public Guid Id { get; set; }

        public Tenant Tenant { get; set; }

        public string Title { get; set; }

        public int Votes { get; set; }

        public DateTimeOffset CreatedAt { get; set; }

        public DateTimeOffset UpdatedAt { get; set; }
    }
}

注意 Tenant 属性。
在数据库中,问题表包含一个 tenant_id 列。
Entity Framework Core 足够聪明,可以确定此属性表示租户和问题之间的一对多关系。
稍后在查询数据时会用到它。

到目前为止,您已经设置了 Entity Framework Core 和与 Citus 的连接。
下一步是向 ASP.NET Core 管道添加多租户支持。

安装 SaasKit

SaasKit 是一款优秀的开源 ASP.NET Core 中间件。
该软件包使您的 Startup 请求管道 租户感知(tenant-aware) 变得容易,
并且足够灵活以处理许多不同的多租户用例。

安装 SaasKit.Multitenancy 包:

dotnet add package SaasKit.Multitenancy

SaasKit 需要两件事才能工作:租户模型(tenant model)和租户解析器(tenant resolver)。
您已经有了前者(您之前创建的 Tenant 类),因此在项目根目录中创建一个名为 CachingTenantResolver.cs 的新文件:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using SaasKit.Multitenancy;
using QuestionExchange.Models;

namespace QuestionExchange
{
    public class CachingTenantResolver : MemoryCacheTenantResolver
 {
 private readonly AppDbContext \_context;

 public CachingTenantResolver(
 AppDbContext context, IMemoryCache cache, ILoggerFactory loggerFactory)
 : base(cache, loggerFactory)
 {
 \_context = context;
 }

 // Resolver runs on cache misses
 protected override async Task> ResolveAsync(HttpContext context)
 {
 var subdomain = context.Request.Host.Host.ToLower();

 var tenant = await \_context.Tenants
 .FirstOrDefaultAsync(t => t.Domain == subdomain);

 if (tenant == null) return null;

 return new TenantContext(tenant);
 }

 protected override MemoryCacheEntryOptions CreateCacheEntryOptions()
 => new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromHours(2));

 protected override string GetContextIdentifier(HttpContext context)
 => context.Request.Host.Host.ToLower();

 protected override IEnumerable<string> GetTenantIdentifiers(TenantContext context)
 => new string[] { context.Tenant.Domain };
 }
}

ResolveAsync 方法完成了繁重的工作:给定传入请求,它会查询数据库并查找与当前域匹配的租户。
如果找到,它会将 TenantContext 传回给 SaasKit。
所有租户解析逻辑完全取决于您 - 您可以按子域、路径或任何其他您想要的方式分隔租户。

此实现使用 MARKDOWN_HASH30e1651b497cdf2a70fe553ea0e69106MARKDOWNHASH _\,
因此您不会在每个传入请求上都使用租户查找来访问数据库。
第一次查找后,租户将被缓存两个小时(您可以将其更改为任何有意义的内容)。

准备好租户模型(tenant model)和租户解析器(tenant resolver)后,
打开 Startup 类并在 ConfigureServices 方法中的任何位置添加此行:

services.AddMultitenancy();

接下来,将此行添加到 Configure 方法中,在 UseStaticFiles 下方但在 UseMvc 上方

app.UseMultitenancy();

Configure 方法代表您的实际请求管道,因此顺序很重要!

更新视图

现在所有部分都已就绪,您可以开始在代码和视图中引用当前租户。
打开 Views/Home/Index.cshtml 视图并用这个标记替换整个文件:

@inject Tenant Tenant
@model QuestionListViewModel

@{
    ViewData["Title"] = "Home Page";
}

<div class="row">
    <div class="col-md-12">
        <h1>Welcome to <strong>@Tenant.Namestrong>h1>
        <h3>@Tenant.Descriptionh3>
    div>
div>

<div class="row">
    <div class="col-md-12">
        <h4>Popular questionsh4>
        <ul>
            @foreach (var question in Model.Questions)
            {
                <li>@question.Titleli>
            }
        ul>
    div>
div>

@inject 指令从 SaasKit 获取当前租户,并且
@model 指令告诉 ASP.NET Core,
此视图将由新模型类(您将创建)支持。
在 Models 目录中创建 QuestionListViewModel.cs 文件:


using System.Collections.Generic;

namespace QuestionExchange.Models
{
    public class QuestionListViewModel
 {
    public IEnumerable Questions { get; set; }
 }
}

查询数据库

HomeController 负责渲染您刚刚编辑的索引视图。打开它并用这个替换 Index() 方法:

public async Task Index()
{
 var topQuestions = await \_context
 .Questions
 .Where(q => q.Tenant.Id == \_currentTenant.Id)
 .OrderByDescending(q => q.UpdatedAt)
 .Take(5)
 .ToArrayAsync();

 var viewModel = new QuestionListViewModel
 {
 Questions = topQuestions
 };

 return View(viewModel);
}

此查询获取此租户的最新五个问题(当然,现在只有一个问题)并填充视图模型。

对于大型应用程序,您通常会将数据访问代码放在 service 或 repository 层中,
并将其置于 controller 之外。 这只是一个简单的例子!

您添加的代码需要 _context_currentTenant,这在 controller 中尚不可用。
您可以通过以下方式提供这些向类添加构造函数:

public class HomeController : Controller
{
    private readonly AppDbContext _context;
    private readonly Tenant _currentTenant;

    public HomeController(AppDbContext context, Tenant tenant)
    {
        _context = context;
        _currentTenant = tenant;
    }

    // Existing code...

为避免编译器报错,请在文件顶部添加以下声明:

using Microsoft.EntityFrameworkCore;

测试应用程序

您添加到数据库的测试租户与(fake)域 bufferoverflow.localdboverflow.local 相关联。
您需要 编辑 hosts 文件 以在本地计算机上测试这些:

127.0.0.1 bufferoverflow.local
127.0.0.1 dboverflow.local

使用 dotnet run 或单击 Visual Studio 中的 Start 启动项目,
应用程序将开始侦听 localhost:5000 之类的 URL。
如果您直接访问该 URL,您将看到一个错误,因为您尚未设置任何 默认租户行为

相反,访问 http://bufferoverflow.local:5000
您将看到您的多租户应用程序的一个租户!
切换到 http://dboverflow.local:5000 查看其他租户。
添加更多租户现在只需在 tenants 表中添加更多行即可。

更多

探索 Python/Django 支持分布式多租户数据库,如 Postgres+Citus

转载请注明:xuhss » ASP.NET Core + SaasKit + PostgreSQL + Citus 的多租户应用程序架构示例

喜欢 (0)

您必须 登录 才能发表评论!