MultiTenancyserver มีจุดมุ่งหมายที่จะเป็นแพ็คเกจที่มีน้ำหนักเบาสำหรับการเพิ่มการรองรับการเช่าหลายรายการให้กับ codebase ใด ๆ ได้อย่างง่ายดาย การออกแบบของมันได้รับอิทธิพลอย่างมากจากอัตลักษณ์หลักของ ASP.NET คุณสามารถเพิ่มการรองรับการเช่าหลายครั้งในโมเดลของคุณโดยไม่ต้องเพิ่มคุณสมบัติคีย์ผู้เช่าใด ๆ ในคลาสหรือเอนทิตีใด ๆ การใช้ ASP.NET Core ผู้เช่าปัจจุบันสามารถเรียกคืนได้โดยชื่อโดเมนที่กำหนดเองโดเมนย่อยชื่อโฮสต์บางส่วนส่วนหัวคำขอ HTTP, เส้นทาง URL เด็กหรือบางส่วน, พารามิเตอร์สตริงแบบสอบถาม, การเรียกร้องผู้ใช้ที่ได้รับการรับรองความถูกต้อง การใช้แกนเฟรมเวิร์กเอนทิตีคีย์ผู้เช่าจะถูกเพิ่มเป็นคุณสมบัติเงา (หรือคุณสมบัติคอนกรีตที่เป็นทางเลือก) และบังคับใช้ผ่านตัวกรองการสืบค้นทั่วโลกตัวเลือกที่กำหนดค่าได้ทั้งหมดสามารถตั้งค่าได้จากค่าเริ่มต้น ตัวอย่างด้านล่างนี้เน้นวิธีการใช้ MultiTenancyServer กับ ASP.NET Core Identity และ IdentityServer4 ด้วยกัน คุณสามารถค้นหาตัวอย่างการทำงานเต็มรูปแบบจำนวนมากที่รวมเข้ากับ IdentityServer4, ASP.NET Core Identity (ใช้ประเภทคีย์ที่แตกต่างกันเช่นสตริงและ INT64) และแกนเฟรมเวิร์กเอนทิตีในตัวอย่าง repo
กำหนดโมเดลผู้เช่าของคุณเองหรือสืบทอดมาจาก TenancyTenant หรือเพียงแค่ใช้ TenancyTenant ตามที่เป็นอยู่ ในตัวอย่างนี้เราจะสืบทอดจาก TenancyTenant และเพิ่มชื่อที่แสดง
public class Tenant : TenancyTenant
{
// Custom property for display name of tenant.
public string Name { get ; set ; }
} ตัวอย่างการเพิ่มการรองรับการเช่าหลายครั้งใน ASP.NET Core
public void ConfigureServices ( IServiceCollection services )
{
var connectionString = Configuration . GetConnectionString ( "DefaultConnection" ) ;
var migrationsAssembly = typeof ( AppDbContext ) . GetTypeInfo ( ) . Assembly . GetName ( ) . Name ;
services . AddDbContext < AppDbContext > ( options =>
{
options . UseSqlServer ( connectionString , sql => sql . MigrationsAssembly ( migrationsAssembly ) ) ;
} ) ;
// Add Multi-Tenancy Server defining TTenant<TKey> as type Tenant with an ID (key) of type string.
services . AddMultiTenancy < Tenant , string > ( )
// Add one or more IRequestParser (MultiTenancyServer.AspNetCore).
. AddRequestParsers ( parsers =>
{
// Parsers are processed in the order they are added,
// typically 1 or 2 parsers should be all you need.
parsers
// www.tenant1.com
. AddDomainParser ( )
// tenant1.tenants.multitenancyserver.io
. AddSubdomainParser ( ".tenants.multitenancyserver.io" )
// from partial hostname
. AddHostnameParser ( "^(regular_expression)$" )
// HTTP header X-TENANT = tenant1
. AddHeaderParser ( "X-TENANT" )
// /tenants/tenant1
. AddChildPathParser ( "/tenants/" )
// from partial path
. AddPathParser ( "^(regular_expression)$" )
// ?tenant=tenant1
. AddQueryParser ( "tenant" )
// Claim from authenticated user principal.
. AddClaimParser ( "http://schemas.microsoft.com/identity/claims/tenantid" )
// Add custom request parser with lambda.
. AddCustomParser ( httpContext => "tenant1" ) ;
// Add custom request parser implementation.
. AddMyCustomParser ( ) ;
} )
// Use in memory tenant store for development (MultiTenancyServer.Stores)
. AddInMemoryStore ( new Tenant [ ]
{
new Tenant ( )
{
Id = "TENANT_1" ,
CanonicalName = "Tenant1" ,
NormalizedCanonicalName = "TENANT1"
}
} )
// Use EF Core store for production (MultiTenancyServer.EntityFrameworkCore).
. AddEntityFrameworkStore < AppDbContext , Tenant , string > ( )
// Use custom store.
. AddMyCustomStore ( ) ;
// Add ASP.NET Core Identity
services . AddIdentity < User , Role > ( )
. AddEntityFrameworkStores < AppDbContext > ( )
. AddDefaultTokenProviders ( ) ;
// Add Identity Server 4
var builder = services . AddIdentityServer ( )
. AddAspNetIdentity < User > ( )
// Add the config data from DB (clients, resources)
. AddConfigurationStore < AppDbContext > ( options =>
{
options . ConfigureDbContext = b =>
b . UseSqlServer ( connectionString ,
sql => sql . MigrationsAssembly ( migrationsAssemblyName ) ) ;
} )
// Add the operational data from DB (codes, tokens, consents)
. AddOperationalStore < AppDbContext > ( options =>
{
options . ConfigureDbContext = b =>
b . UseSqlServer ( connectionString ,
sql => sql . MigrationsAssembly ( migrationsAssemblyName ) ) ;
} ) ;
if ( Environment . IsDevelopment ( ) )
{
builder . AddDeveloperSigningCredential ( ) ;
}
else
{
throw new Exception ( "Key not configured." ) ;
}
} ตัวอย่างการกำหนดค่าแอปพลิเคชันด้วยการรองรับการเช่าแบบหลายครั้งสำหรับ ASP.NET Core MVC และ IdentityServer4
public void Configure ( IApplicationBuilder app )
{
// other code removed for brevity
app . UseStaticFiles ( ) ;
app . UseMultiTenancy < Tenant > ( ) ;
app . UseIdentityServer ( ) ;
app . UseAuthentication ( ) ;
app . UseMvcWithDefaultRoute ( ) ;
} ตัวอย่างของ DBContext ที่มีการรองรับการเช่าหลายครั้งสำหรับ ASP.NET Core Identity และ IdentityServer4
public class AppDbContext :
// ASP.NET Core Identity EF Core
IdentityDbContext < User , Role , string , UserClaim , UserRole , UserLogin , RoleClaim , UserToken > ,
// IdentityServer4 EF Core
IConfigurationDbContext , IPersistedGrantDbContext ,
// MultiTenancyServer EF Core
ITenantDbContext < Tenant , string >
{
private static object _tenancyModelState ;
private readonly ITenancyContext < Tenant > _tenancyContext ;
public AppDbContext (
DbContextOptions < AppDbContext > options ,
ITenancyContext < Tenant > tenancyContext )
: base ( options )
{
// The request scoped tenancy context.
// Should not access the tenancyContext.Tenant property in the constructor yet,
// as the request pipeline has not finished running yet and it will likely be null.
_tenancyContext = tenancyContext ;
}
// IdentityServer4 implementation.
public DbSet < Client > Clients { get ; set ; }
public DbSet < IdentityResource > IdentityResources { get ; set ; }
public DbSet < ApiResource > ApiResources { get ; set ; }
public DbSet < PersistedGrant > PersistedGrants { get ; set ; }
// MultiTenancyServer implementation.
public DbSet < Tenant > Tenants { get ; set ; }
protected override void OnModelCreating ( ModelBuilder builder )
{
base . OnModelCreating ( builder ) ;
// IdentityServer4 configuration.
var configurationStoreOptions = new ConfigurationStoreOptions ( ) ;
builder . ConfigureClientContext ( configurationStoreOptions ) ;
builder . ConfigureResourcesContext ( configurationStoreOptions ) ;
var operationalStoreOptions = new OperationalStoreOptions ( ) ;
builder . ConfigurePersistedGrantContext ( operationalStoreOptions ) ;
// MultiTenancyServer configuration.
var tenantStoreOptions = new TenantStoreOptions ( ) ;
builder . ConfigureTenantContext < Tenant , string > ( tenantStoreOptions ) ;
// Add multi-tenancy support to model.
var tenantReferenceOptions = new TenantReferenceOptions ( ) ;
builder . HasTenancy < string > ( tenantReferenceOptions , out _tenancyModelState ) ;
// Configure custom properties on Tenant (MultiTenancyServer).
builder . Entity < Tenant > ( b =>
{
b . Property ( t => t . Name ) . HasMaxLength ( 256 ) ;
} ) ;
// Configure properties on User (ASP.NET Core Identity).
builder . Entity < User > ( b =>
{
// Add multi-tenancy support to entity.
b . HasTenancy ( ( ) => _tenantId , _tenancyModelState , hasIndex : false ) ;
// Remove unique index on NormalizedUserName.
b . HasIndex ( u => u . NormalizedUserName ) . HasName ( "UserNameIndex" ) . IsUnique ( false ) ;
// Add unique index on TenantId and NormalizedUserName.
b . HasIndex ( tenantReferenceOptions . ReferenceName , nameof ( User . NormalizedUserName ) )
. HasName ( "TenantUserNameIndex" ) . IsUnique ( ) ;
} ) ;
// Configure properties on Role (ASP.NET Core Identity).
builder . Entity < Role > ( b =>
{
// Add multi-tenancy support to entity.
b . HasTenancy ( ( ) => _tenancyContext . Tenant . Id , _tenancyModelState , hasIndex : false ) ;
// Remove unique index on NormalizedName.
b . HasIndex ( r => r . NormalizedName ) . HasName ( "RoleNameIndex" ) . IsUnique ( false ) ;
// Add unique index on TenantId and NormalizedName.
b . HasIndex ( tenantReferenceOptions . ReferenceName , nameof ( Role . NormalizedName ) )
. HasName ( "TenantRoleNameIndex" ) . IsUnique ( ) ;
} ) ;
// Configure properties on Client (IdentityServer4).
builder . Entity < Client > ( b =>
{
// Add multi-tenancy support to entity.
b . HasTenancy ( ( ) => _tenancyContext . Tenant . Id , _tenancyModelState , hasIndex : false ) ;
// Remove unique index on ClientId.
b . HasIndex ( c => c . ClientId ) . IsUnique ( false ) ;
// Add unique index on TenantId and ClientId.
b . HasIndex ( tenantReferenceOptions . ReferenceName , nameof ( Client . ClientId ) ) . IsUnique ( ) ;
} ) ;
// Configure properties on IdentityResource (IdentityServer4).
builder . Entity < IdentityResource > ( b =>
{
// Add multi-tenancy support to entity.
b . HasTenancy ( ( ) => _tenancyContext . Tenant . Id , _tenancyModelState , hasIndex : false ) ;
// Remove unique index on Name.
b . HasIndex ( r => r . Name ) . IsUnique ( false ) ;
// Add unique index on TenantId and Name.
b . HasIndex ( tenantReferenceOptions . ReferenceName , nameof ( IdentityResource . Name ) ) . IsUnique ( ) ;
} ) ;
// Configure properties on ApiResource (IdentityServer4).
builder . Entity < ApiResource > ( b =>
{
// Add multi-tenancy support to entity.
b . HasTenancy ( ( ) => _tenancyContext . Tenant . Id , _tenancyModelState , hasIndex : false ) ;
// Remove unique index on Name.
b . HasIndex ( r => r . Name ) . IsUnique ( false ) ;
// Add unique index on TenantId and Name.
b . HasIndex ( tenantReferenceOptions . ReferenceName , nameof ( ApiResource . Name ) ) . IsUnique ( ) ;
} ) ;
// Configure properties on ApiScope (IdentityServer4).
builder . Entity < ApiScope > ( b =>
{
// Add multi-tenancy support to entity.
b . HasTenancy ( ( ) => _tenancyContext . Tenant . Id , _tenancyModelState , hasIndex : false ) ;
// Remove unique index on Name.
b . HasIndex ( s => s . Name ) . IsUnique ( false ) ;
// Add unique index on TenantId and Name.
b . HasIndex ( tenantReferenceOptions . ReferenceName , nameof ( ApiScope . Name ) ) . IsUnique ( ) ;
} ) ;
// Configure properties on PersistedGrant (IdentityServer4).
builder . Entity < PersistedGrant > ( b =>
{
// Add multi-tenancy support to entity.
b . HasTenancy ( ( ) => _tenancyContext . Tenant . Id , _tenancyModelState ) ;
} ) ;
}
public override int SaveChanges ( bool acceptAllChangesOnSuccess )
{
// Ensure multi-tenancy for all tenantable entities.
this . EnsureTenancy ( _tenancyContext ? . Tenant ? . Id , _tenancyModelState , _logger ) ;
return base . SaveChanges ( acceptAllChangesOnSuccess ) ;
}
public override Task < int > SaveChangesAsync ( bool acceptAllChangesOnSuccess , CancellationToken cancellationToken = default )
{
// Ensure multi-tenancy for all tenantable entities.
this . EnsureTenancy ( _tenancyContext ? . Tenant ? . Id , _tenancyModelState , _logger ) ;
return base . SaveChangesAsync ( acceptAllChangesOnSuccess , cancellationToken ) ;
}
} public class TenantReferenceOptions
{
// Summary:
// If set to a non-null value, the store will use this value as the name for the
// tenant's reference property. The default is "TenantId".
public string ReferenceName { get ; set ; }
// Summary:
// True to enable indexing of tenant reference properties in the store, otherwise
// false. The default is true.
public bool IndexReferences { get ; set ; }
// Summary:
// If set to a non-null value, the store will use this value as the name of the
// index for any tenant references. The name is also a format pattern of {0:PropertyName}.
// The default is "{0}Index", eg. "TenantIdIndex".
public string IndexNameFormat { get ; set ; }
// Summary:
// Determines if a null tenant reference is allowed for entities and how querying
// for null tenant references is handled.
public NullTenantReferenceHandling NullTenantReferenceHandling { get ; set ; }
}
public enum NullTenantReferenceHandling
{
// Summary:
// A null tenant reference is NOT allowed for the entity, where possible a NOT NULL
// or REQUIRED constraint should be set on the tenant reference, querying for entities
// with a null tenant reference will match NO entities.
// This is the default option.
NotNullDenyAccess = 0 ,
// Summary:
// A null tenant reference is allowed for the entity, where possible an NULLABLE
// or OPTIONAL constraint should be set on the tenant reference, querying for entities
// with a null tenant reference will match those expected results.
// This may be useful where globally defined system entities are set with a null
// tenant reference.
NullableEntityAccess = 1 ,
// Summary:
// A null tenant reference is NOT allowed for the entity, where possible a NOT NULL
// or REQUIRED constraint should be set on the tenant reference, querying for entities
// with a null tenant reference will match ALL entities across all tenants.
// For obvious security reasons, this is typically not recommended; however, this
// can be useful for admin reporting across all tenants.
NotNullGlobalAccess = 2
} microsoft.aspnetcore.hosting.internal.webhost: ข้อมูล: คำขอเริ่มต้น http/1.1 โพสต์ http: // localhost : 5020/บัญชี/เข้าสู่ระบบ
microsoft.entityframeworkcore.database.Command: ข้อมูล: ดำเนินการ dbcommand (3ms) [พารามิเตอร์ = [@__ normalizedCanonicalName_0 = ' localhost ' (ขนาด = 256)], commandType = 'ข้อความ', commandtimout = '30 ' [u]. [Concurrencystamp], [u]. [ชื่อ], [u]. [NormalizedCanonicalName] จาก [ผู้เช่า] เป็น [u] โดยที่ [u].
MultiTenancyserver.http.htttptenancyProvider: DEBUG: ผู้เช่า test_Tenant_1 พบโดย DomainParser สำหรับค่า localhost ในคำขอ http: // localhost : 5020/บัญชี/เข้าสู่ระบบ? returnUrl =%2fgrants
microsoft.entityframeworkcore.database.Command: ข้อมูล: ดำเนินการ dbcommand (13ms) [พารามิเตอร์ = [@___ tenantid_0 = ' test_tenant_1 ' (ขนาด = 4000), @__ normalizeRname_0 = 'Alice' (ขนาด = 256) ... , [u]. [ชื่อผู้ใช้] จาก [ผู้ใช้] เป็น [u] โดยที่ (@___ tenantid_0 ไม่ได้เป็นโมฆะและ ([u]. [tenantid] = @___ tenantid_0) และ ([u]
Microsoft.entityFrameworkcore.database.Command: ข้อมูล: ดำเนินการ dbcommand (1ms) [พารามิเตอร์ = [@___ tenantid_0 = ' test_tenant_1 ' (ขนาด = 4000), @__ subjectId_0 = '3AB99036-8AC1-4270-8 commandType = 'text', commandtimeout = '30 '] เลือก [p]. [key], [p]. [clientId], [p]. [การสร้าง], [p]. [data], [p]. [หมดอายุ], [p] . ([p]. [tenantid] = @___ tenantid_0)) และ ([p]. [subjectId] = @__ subjectId_0)