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 DbContext
OnModelCreating
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 DbContext
dalam :
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.