Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
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-tenant" karena setiap pelanggan dianggap sebagai penyewa aplikasi dengan data mereka masing-masing.
Peringatan
Artikel ini menggunakan database lokal yang tidak mengharuskan pengguna untuk diautentikasi. Aplikasi produksi harus menggunakan alur autentikasi paling aman yang tersedia. Untuk informasi selengkapnya tentang autentikasi untuk aplikasi pengujian dan produksi yang disebarkan, lihat Mengamankan alur autentikasi.
Penting
Dokumen ini menyediakan contoh dan solusi "apa adanya." Ini tidak dimaksudkan untuk menjadi "praktik terbaik" melainkan "praktik kerja" untuk pertimbangan Anda.
Petunjuk
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 berdasarkan 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 dapat semudah menyediakan 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 tidak masalah karena meskipun pabrik diambil bersama, instans individu DbContext
tidak.
Namun, untuk multi-tenancy, string sambungan dapat berubah untuk setiap 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 melebihi permintaan awal. Pabrik baru harus disediakan untuk setiap pengguna untuk memungkinkan pengaturan baru. Masa hidup untuk pabrik khusus ini terbatas dan instans baru dibuat per sesi pengguna.
Contoh solusi (database tunggal)
Solusi yang mungkin adalah membuat layanan sederhana ITenantService
yang menangani pengaturan tenant 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 diubah untuk menetapkan 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 mengambil ketergantungan pada penyedia penyewa.
Catatan
Dependensi harus selalu mengalir ke singleton. Itu berarti layanan Scoped
dapat bergantung pada layanan Scoped
lain atau layanan Singleton
, tetapi layanan Singleton
hanya dapat bergantung pada layanan lain Singleton
: 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 pengguna. Ini dapat dikonfigurasi saat startup dengan menentukan penyedia layanan dan menggunakannya untuk membangun string koneksi yang diperlukan. Bagian string koneksi untuk setiap penyewa telah ditambahkan ke dalam file konfigurasi appsettings.json
.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"TenantA": "Data Source=tenantacontacts.sqlite",
"TenantB": "Data Source=tenantbcontacts.sqlite"
},
"AllowedHosts": "*"
}
Layanan dan konfigurasi disuntikkan ke dalam DbContext
.
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 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 dengan mengatur usia pakai menjadi Transient.
. Ini memastikan bahwa penyewa dievaluasi ulang 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
tetap masuk akal jika database Anda tidak memiliki dependensi yang bersifat cakupan pengguna.
Catatan performa
EF Core dirancang agar instans DbContext
dapat diinstansiasi dengan cepat dengan overhead sesedikit mungkin. Untuk alasan itu, membuat DbContext
baru per operasi biasanya tidak masalah. Jika pendekatan ini memengaruhi performa aplikasi Anda, pertimbangkan untuk menggunakan penggunaan bersama DbContext.
Kesimpulan
Ini adalah pedoman kerja untuk menerapkan multi-tentasi dalam aplikasi EF Core. Jika Anda memiliki contoh atau skenario lebih lanjut atau ingin memberikan umpan balik, silakan buka masalah dan referensikan dokumen ini.