Bagikan melalui


Multi-tenancy

Banyak lini aplikasi bisnis dirancang untuk bekerja dengan beberapa pelanggan. Penting untuk mengamankan data sehingga data pelanggan tidak "bocor" atau dilihat oleh pelanggan lain dan pesaing potensial. Aplikasi ini diklasifikasikan sebagai "multi-penyewa" karena setiap pelanggan dianggap sebagai penyewa aplikasi dengan sekumpulan data mereka sendiri.

Penting

Dokumen ini menyediakan contoh dan solusi "apa adanya." Ini tidak dimaksudkan untuk menjadi "praktik terbaik" melainkan "praktik kerja" untuk pertimbangan Anda.

Tip

Anda dapat melihat kode sumber untuk sampel ini di GitHub

Mendukung multi-penyewaan

Ada banyak pendekatan untuk menerapkan multi-penyewaan dalam aplikasi. Salah satu pendekatan umum (yang terkadang menjadi persyaratan) adalah menyimpan data untuk setiap pelanggan dalam database terpisah. Skemanya sama tetapi datanya khusus pelanggan. Pendekatan lain adalah mempartisi data dalam database yang ada oleh pelanggan. Ini dapat dilakukan dengan menggunakan kolom dalam tabel, atau memiliki tabel dalam beberapa skema dengan skema untuk setiap penyewa.

Pendekatan Kolom untuk Penyewa? Skema per Penyewa? Beberapa Database? Dukungan Inti EF
Diskriminator (kolom) Ya Tidak Tidak Filter kueri global
Database per penyewa Tidak Tidak Ya Konfigurasi
Skema per penyewa Tidak Ya Tidak Tidak didukung

Untuk pendekatan database per penyewa, beralih ke database yang tepat semestinya memberikan string koneksi yang benar. Saat data disimpan dalam database tunggal, filter kueri global dapat digunakan untuk memfilter baris secara otomatis menurut kolom ID penyewa, memastikan bahwa pengembang tidak secara tidak sengaja menulis kode yang dapat mengakses data dari pelanggan lain.

Contoh-contoh ini harus berfungsi dengan baik di sebagian besar model aplikasi, termasuk konsol, WPF, WinForms, dan aplikasi ASP.NET Core. Aplikasi Blazor Server memerlukan pertimbangan khusus.

Aplikasi Blazor Server dan masa pakai pabrik

Pola yang direkomendasikan untuk menggunakan Entity Framework Core di aplikasi Blazor adalah mendaftarkan DbContextFactory, lalu memanggilnya untuk membuat instans baru dari DbContext setiap operasi. Secara default, pabrik adalah singleton sehingga hanya satu salinan yang ada untuk semua pengguna aplikasi. Ini biasanya baik-baik saja karena meskipun pabrik dibagikan, instans individu DbContext tidak.

Namun, untuk multi-penyewaan, string koneksi dapat berubah per pengguna. Karena pabrik menyimpan konfigurasi dengan masa pakai yang sama, ini berarti semua pengguna harus berbagi konfigurasi yang sama. Oleh karena itu, masa pakai harus diubah menjadi Scoped.

Masalah ini tidak terjadi di aplikasi Blazor WebAssembly karena singleton dicakup ke pengguna. Di sisi lain, aplikasi Blazor Server menghadirkan tantangan unik. Meskipun aplikasi ini adalah aplikasi web, aplikasi ini "tetap hidup" dengan komunikasi real time menggunakan SignalR. Sesi dibuat per pengguna dan berlangsung di luar permintaan awal. Pabrik baru harus disediakan per pengguna untuk mengizinkan pengaturan baru. Masa pakai untuk pabrik khusus ini terlingkup dan instans baru dibuat per sesi pengguna.

Contoh solusi (database tunggal)

Solusi yang mungkin adalah membuat layanan sederhana ITenantService yang menangani pengaturan penyewa pengguna saat ini. Ini menyediakan panggilan balik sehingga kode diberi tahu saat penyewa berubah. Implementasi (dengan panggilan balik yang dihilangkan untuk kejelasan) mungkin terlihat seperti ini:

namespace Common
{
    public interface ITenantService
    {
        string Tenant { get; }

        void SetTenant(string tenant);

        string[] GetTenants();

        event TenantChangedEventHandler OnTenantChanged;
    }
}

Kemudian DbContext dapat mengelola multi-penyewaan. Pendekatan tergantung pada strategi database Anda. Jika Anda menyimpan semua penyewa dalam satu database, Anda mungkin akan menggunakan filter kueri. diteruskan ITenantService ke konstruktor melalui injeksi dependensi dan digunakan untuk menyelesaikan dan menyimpan pengidentifikasi penyewa.

public ContactContext(
    DbContextOptions<ContactContext> opts,
    ITenantService service)
    : base(opts) => _tenant = service.Tenant;

Metode OnModelCreating ini ditimpa untuk menentukan filter kueri:

protected override void OnModelCreating(ModelBuilder modelBuilder)
    => modelBuilder.Entity<MultitenantContact>()
        .HasQueryFilter(mt => mt.Tenant == _tenant);

Ini memastikan bahwa setiap kueri difilter ke penyewa pada setiap permintaan. Tidak perlu memfilter dalam kode aplikasi karena filter global akan diterapkan secara otomatis.

Penyedia penyewa dan DbContextFactory dikonfigurasi dalam startup aplikasi seperti ini, menggunakan Sqlite sebagai contoh:

builder.Services.AddDbContextFactory<ContactContext>(
    opt => opt.UseSqlite("Data Source=singledb.sqlite"), ServiceLifetime.Scoped);

Perhatikan bahwa masa pakai layanan dikonfigurasi dengan ServiceLifetime.Scoped. Ini memungkinkannya untuk mengambil dependensi pada penyedia penyewa.

Catatan

Dependensi harus selalu mengalir ke singleton. Itu berarti Scoped layanan dapat bergantung pada layanan atau Singleton layanan lainScoped, tetapi Singleton layanan hanya dapat bergantung pada layanan lainSingleton: Transient => Scoped => Singleton.

Beberapa skema

Peringatan

Skenario ini tidak didukung langsung oleh EF Core dan bukan solusi yang direkomendasikan.

Dalam pendekatan yang berbeda, database yang sama dapat menangani tenant1 dan tenant2 dengan menggunakan skema tabel.

  • Penyewa1 - tenant1.CustomerData
  • Penyewa2 - tenant2.CustomerData

Jika Anda tidak menggunakan EF Core untuk menangani pembaruan database dengan migrasi dan sudah memiliki tabel multi-skema, Anda dapat mengambil alih skema dalam DbContextOnModelCreating hal seperti ini (skema untuk tabel CustomerData diatur ke penyewa):

protected override void OnModelCreating(ModelBuilder modelBuilder) =>
    modelBuilder.Entity<CustomerData>().ToTable(nameof(CustomerData), tenant);

Beberapa database dan string koneksi

Beberapa versi database diimplementasikan dengan meneruskan string koneksi yang berbeda untuk setiap penyewa. Ini dapat dikonfigurasi saat startup dengan menyelesaikan penyedia layanan dan menggunakannya untuk membangun string koneksi. Bagian string koneksi menurut penyewa ditambahkan ke appsettings.json file konfigurasi.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "ConnectionStrings": {
    "TenantA": "Data Source=tenantacontacts.sqlite",
    "TenantB": "Data Source=tenantbcontacts.sqlite"
  },
  "AllowedHosts": "*"
}

Layanan dan konfigurasi keduanya disuntikkan ke DbContextdalam :

public ContactContext(
    DbContextOptions<ContactContext> opts,
    IConfiguration config,
    ITenantService service)
    : base(opts)
{
    _tenantService = service;
    _configuration = config;
}

Penyewa kemudian digunakan untuk mencari string koneksi di OnConfiguring:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    var tenant = _tenantService.Tenant;
    var connectionStr = _configuration.GetConnectionString(tenant);
    optionsBuilder.UseSqlite(connectionStr);
}

Ini berfungsi dengan baik untuk sebagian besar skenario kecuali pengguna dapat beralih penyewa selama sesi yang sama.

Beralih penyewa

Dalam konfigurasi sebelumnya untuk beberapa database, opsi di-cache di tingkat tersebut Scoped . Ini berarti bahwa jika pengguna mengubah penyewa, opsi tidak dievaluasi ulang dan sehingga perubahan penyewa tidak tercermin dalam kueri.

Solusi mudah untuk ini ketika penyewa dapat berubah adalah mengatur masa pakai ke Transient. Ini memastikan penyewa dievaluasi kembali bersama dengan string koneksi setiap kali DbContext diminta. Pengguna dapat mengalihkan penyewa sesering yang mereka suka. Tabel berikut membantu Anda memilih masa pakai mana yang paling masuk akal untuk pabrik Anda.

Skenario Database tunggal Beberapa database
Pengguna tetap berada dalam satu penyewa Scoped Scoped
Pengguna dapat beralih penyewa Scoped Transient

Default Singleton masih masuk akal jika database Anda tidak mengambil dependensi cakupan pengguna.

Catatan performa

EF Core dirancang agar DbContext instans dapat diinstansiasi dengan cepat dengan overhead sesedikitan mungkin. Untuk alasan itu, membuat baru DbContext per operasi biasanya harus baik-baik saja. Jika pendekatan ini memengaruhi performa aplikasi Anda, pertimbangkan untuk menggunakan pengumpulan DbContext.

Kesimpulan

Ini adalah panduan kerja untuk menerapkan multi-penyewaan di aplikasi EF Core. Jika Anda memiliki contoh atau skenario lebih lanjut atau ingin memberikan umpan balik, silakan buka masalah dan referensikan dokumen ini.