使用 IPageRouteModelConvention 和单独的租户页面文件夹为多租户配置 Razor 页面
Configure Razor Pages for multi-tenancy, using IPageRouteModelConvention and separate tenant page folders
我正在尝试配置 Razor Pages 路由(不是 Razor Views)并使其通过目录结构支持多租户...
所以,我会为不同的租户准备一堆页面,例如:
/Pages/1/About.cshtml
/Pages/2/About.cshtml
/Pages/2/Other.cshtml
... plus many many more ...
和 tenantID 查找的来源,即:
"example.com" : 1,
"site.tld" : 2,
...
然后,当有人请求“example.com/About”时,它会正确映射到租户 #1 子文件夹中的页面(因为“example.com”映射到上面示例中的 #1),而不是不同租户的“关于”页面。
废弃的解决方案...
- 有很多 Razor View 解决方案,但这不是我要找的(我正在使用 Razor PAGES)。
- 我也看到有人使用 url-rewriting,但这有点蛮力且不够优雅,我想要一个合适的路由解决方案。
- 硬编码路由显然可行(在映射或页面指令中),但这不可扩展且容易出错。
可能的解决方案?
使用 IPageRouteModelConvention 似乎是配置 Razor Pages 路由的“正确”方式?
似乎我可以修改路由选择器以剥离租户 ID 子目录,从而使页面在根路径上可用。但是,我还需要确保请求的是适当的租户页面,而不是其他租户的...
一种方法(我认为)是使用 ActionConstraint(也可以在 IPageRouteModelConvention 中配置)。如果 origin:tenantId 字典是硬编码的,那么我认为这很容易......但是我的租户查找数据需要从数据库中提取(我实际上在 .NET Core 中添加了一个 TenantCollection 服务作为单例已收集服务)。
问题 是我无法在 builder.Services.Configure(...) 调用时访问 ServiceProvider(以获取我的 TenantCollection)。
所以我无法创建 ActionConstraint 来限制对某些来源的某些页面的访问,因为我没有租户映射数据。
这是一些示例代码,以防它有助于说明...
builder.Services.AddSingleton<TenantCollection>();
builder.Services.AddRazorPages();
builder.Services.Configure<RazorPagesOptions>(options =>
{
var tenantCollection = GET_MY_TENANT_COLLECTION; // Cant do?
options.Conventions.Add(new MultiTenantPageRouteModelConvention(tenantCollection));
});
我觉得我遗漏了一些明显的东西,或者从错误的方向解决问题?
using Microsoft.AspNetCore.Mvc.RazorPages;
using WebAppRazor.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<TestService>();
// Add services to the container.
builder.Services.AddRazorPages();
ServiceProvider serviceProvider = builder.Services.BuildServiceProvider();
var userRepository = serviceProvider.GetService<TestService>();
var a = userRepository.getString();
我有一个测试服务,它将 return 一个字符串。然后这段代码对我有用,我可以通过上面的代码调用这个服务。
所以,最后我“遗漏了一些明显的东西”。
在 ActionConstraint 中,可以通过 ActionConstraintContext 的 RouteContext.HttpContext.RequestServices 引用访问 ServiceProvider。
这使我能够获得所需的服务来完成所需的工作。简单。
与其就此打住,不如让这个 post 更有价值。未来的人觉得有用。
Program.cs
...
builder.Services.AddSingleton<MyTenantCollection>();
builder.Services.AddScoped(MyTenant.ImplementationFactoryBasedOnRequestOrigin);
builder.Services.Configure<RazorPagesOptions>(options =>
{
options.Conventions.Add(new MyPageRouteModelConvention());
});
...
MyPageRouteModelConvention.cs
...
public class MyPageRouteModelConvention : IPageRouteModelConvention
{
public void Apply(PageRouteModel model)
{
// Only modify pages in the tenants folder.
if (!model.ViewEnginePath.StartsWith("/Tenants/"))
return;
// Tenants/<num>/<page>...
if (!validateAndParseTenantFolderNumFromPath(model.ViewEnginePath, out int tenantFolderNum))
return;
var constraint = new MyTenantActionConstraint(tenantFolderNum);
foreach (var selector in model.Selectors)
{
// Change the selector route template so the page is
// accessible as if it was in the root path.
// Eg "Tenants/123/SomePage" changes to "SomePage"
selector.AttributeRouteModel.Template =
stripOffTheTenantPath(selector.AttributeRouteModel.Template);
// Note that this is directly modifying this selector's route template,
// so it will no longer be accessible from the tenant sub folder path.
// Alternatively one could create a new selector based on this
// one, modify the template in the same way, and add it as a new
// selector to the model.Selectors collection.
// Add the constraint which will restrict the page from being
// chosen unless the request's origin matches the tenant
// (ie: folderNum == tenantId).
selector.ActionConstraints.Add(constraint);
}
}
}
...
MyTenantActionConstraint.cs
...
public class MyTenantActionConstraint : IActionConstraint
{
public int Order => 0;
private readonly int _tenantID;
public MyTenantActionConstraint(int tenantID)
{
_tenantID = tenantID;
}
public bool Accept(ActionConstraintContext context)
{
// Get the MyTenant that matches the current requests origin
// using the MyTenant.ImplementationFactoryBasedOnRequestOrigin.
// This is a 'scoped' service so it only needs to do it once per request.
// Alternatively one could just get the MyTenantCollection and find the
// tenant by _tenantID and then check that your tenant.ExpectedOrigin matches
// the current HttpContext.Request.Host, but that would run
// every time MyTenantActionConstraint.Accept is invoked.
var tenant =
context.RouteContext.HttpContext.RequestServices.GetService(
typeof(MyTenant)) as MyTenant;
// Return whether or not this ActionConstraint and more importantly
// the Page/Route this ActionConstraint is attached to
// is within the tenant folder (eg Pages/Tenants/123/About.cshtml)
// which has the same number (eg 123) as the tenant Id that
// corresponds to the tenant that matches the current request's
// origin (ie tenantWithId123.DomainName == currentRequest.Host),
// meaning.. true/false this page-route is for this tenant.
return tenant?.Id == _tenantID;
}
}
...
我正在尝试配置 Razor Pages 路由(不是 Razor Views)并使其通过目录结构支持多租户...
所以,我会为不同的租户准备一堆页面,例如:
/Pages/1/About.cshtml
/Pages/2/About.cshtml
/Pages/2/Other.cshtml
... plus many many more ...
和 tenantID 查找的来源,即:
"example.com" : 1,
"site.tld" : 2,
...
然后,当有人请求“example.com/About”时,它会正确映射到租户 #1 子文件夹中的页面(因为“example.com”映射到上面示例中的 #1),而不是不同租户的“关于”页面。
废弃的解决方案...
- 有很多 Razor View 解决方案,但这不是我要找的(我正在使用 Razor PAGES)。
- 我也看到有人使用 url-rewriting,但这有点蛮力且不够优雅,我想要一个合适的路由解决方案。
- 硬编码路由显然可行(在映射或页面指令中),但这不可扩展且容易出错。
可能的解决方案?
使用 IPageRouteModelConvention 似乎是配置 Razor Pages 路由的“正确”方式?
似乎我可以修改路由选择器以剥离租户 ID 子目录,从而使页面在根路径上可用。但是,我还需要确保请求的是适当的租户页面,而不是其他租户的...
一种方法(我认为)是使用 ActionConstraint(也可以在 IPageRouteModelConvention 中配置)。如果 origin:tenantId 字典是硬编码的,那么我认为这很容易......但是我的租户查找数据需要从数据库中提取(我实际上在 .NET Core 中添加了一个 TenantCollection 服务作为单例已收集服务)。
问题 是我无法在 builder.Services.Configure(...) 调用时访问 ServiceProvider(以获取我的 TenantCollection)。 所以我无法创建 ActionConstraint 来限制对某些来源的某些页面的访问,因为我没有租户映射数据。
这是一些示例代码,以防它有助于说明...
builder.Services.AddSingleton<TenantCollection>();
builder.Services.AddRazorPages();
builder.Services.Configure<RazorPagesOptions>(options =>
{
var tenantCollection = GET_MY_TENANT_COLLECTION; // Cant do?
options.Conventions.Add(new MultiTenantPageRouteModelConvention(tenantCollection));
});
我觉得我遗漏了一些明显的东西,或者从错误的方向解决问题?
using Microsoft.AspNetCore.Mvc.RazorPages;
using WebAppRazor.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<TestService>();
// Add services to the container.
builder.Services.AddRazorPages();
ServiceProvider serviceProvider = builder.Services.BuildServiceProvider();
var userRepository = serviceProvider.GetService<TestService>();
var a = userRepository.getString();
我有一个测试服务,它将 return 一个字符串。然后这段代码对我有用,我可以通过上面的代码调用这个服务。
所以,最后我“遗漏了一些明显的东西”。 在 ActionConstraint 中,可以通过 ActionConstraintContext 的 RouteContext.HttpContext.RequestServices 引用访问 ServiceProvider。 这使我能够获得所需的服务来完成所需的工作。简单。
与其就此打住,不如让这个 post 更有价值。未来的人觉得有用。
Program.cs
...
builder.Services.AddSingleton<MyTenantCollection>();
builder.Services.AddScoped(MyTenant.ImplementationFactoryBasedOnRequestOrigin);
builder.Services.Configure<RazorPagesOptions>(options =>
{
options.Conventions.Add(new MyPageRouteModelConvention());
});
...
MyPageRouteModelConvention.cs
...
public class MyPageRouteModelConvention : IPageRouteModelConvention
{
public void Apply(PageRouteModel model)
{
// Only modify pages in the tenants folder.
if (!model.ViewEnginePath.StartsWith("/Tenants/"))
return;
// Tenants/<num>/<page>...
if (!validateAndParseTenantFolderNumFromPath(model.ViewEnginePath, out int tenantFolderNum))
return;
var constraint = new MyTenantActionConstraint(tenantFolderNum);
foreach (var selector in model.Selectors)
{
// Change the selector route template so the page is
// accessible as if it was in the root path.
// Eg "Tenants/123/SomePage" changes to "SomePage"
selector.AttributeRouteModel.Template =
stripOffTheTenantPath(selector.AttributeRouteModel.Template);
// Note that this is directly modifying this selector's route template,
// so it will no longer be accessible from the tenant sub folder path.
// Alternatively one could create a new selector based on this
// one, modify the template in the same way, and add it as a new
// selector to the model.Selectors collection.
// Add the constraint which will restrict the page from being
// chosen unless the request's origin matches the tenant
// (ie: folderNum == tenantId).
selector.ActionConstraints.Add(constraint);
}
}
}
...
MyTenantActionConstraint.cs
...
public class MyTenantActionConstraint : IActionConstraint
{
public int Order => 0;
private readonly int _tenantID;
public MyTenantActionConstraint(int tenantID)
{
_tenantID = tenantID;
}
public bool Accept(ActionConstraintContext context)
{
// Get the MyTenant that matches the current requests origin
// using the MyTenant.ImplementationFactoryBasedOnRequestOrigin.
// This is a 'scoped' service so it only needs to do it once per request.
// Alternatively one could just get the MyTenantCollection and find the
// tenant by _tenantID and then check that your tenant.ExpectedOrigin matches
// the current HttpContext.Request.Host, but that would run
// every time MyTenantActionConstraint.Accept is invoked.
var tenant =
context.RouteContext.HttpContext.RequestServices.GetService(
typeof(MyTenant)) as MyTenant;
// Return whether or not this ActionConstraint and more importantly
// the Page/Route this ActionConstraint is attached to
// is within the tenant folder (eg Pages/Tenants/123/About.cshtml)
// which has the same number (eg 123) as the tenant Id that
// corresponds to the tenant that matches the current request's
// origin (ie tenantWithId123.DomainName == currentRequest.Host),
// meaning.. true/false this page-route is for this tenant.
return tenant?.Id == _tenantID;
}
}
...