简体   繁体   English

为多租户配置 Razor 页面,使用 IPageRouteModelConvention 和单独的租户页面文件夹

[英]Configure Razor Pages for multi-tenancy, using IPageRouteModelConvention and separate tenant page folders

I'm trying to configure Razor Pages routing (not Razor Views) and make it support multi-tenancy via directory-structure...我正在尝试配置Razor 页面路由(不是 Razor 视图)并使其通过目录结构支持多租户...

So, I'd have a bunch of pages for different tenants, eg:所以,我会为不同的租户准备一堆页面,例如:

/Pages/1/About.cshtml
/Pages/2/About.cshtml
/Pages/2/Other.cshtml
... plus many many more ...

and an origin to tenantID lookup, ie:以及 tenantID 查找的来源,即:

"example.com" : 1,
"site.tld"    : 2,
...

Then when someone requests "example.com/About" it maps correctly to page in the tenant # 1 subfolder (since "example.com" maps to # 1 in example above), rather than a different tenant "About" page.然后,当有人请求“example.com/About”时,它会正确映射到租户#1 子文件夹中的页面(因为“example.com”映射到上面示例中的#1),而不是不同的租户“关于”页面。

Discarded solutions...废弃的解决方案...

  • There are a bunch of Razor View solutions but that's not what I'm looking for (I'm using Razor PAGES).有一堆 Razor 查看解决方案,但这不是我要找的(我正在使用 Razor PAGES)。
  • Also I've seen one person use url-rewriting, but this is a bit brute-force and inelegant and I'd like a proper routing solution.我也见过一个人使用 url-rewriting,但这有点蛮力和不雅,我想要一个合适的路由解决方案。
  • Hardcoding routes would obviously work (either in a mapping or in page directives) but this is not scalable and is error-prone.硬编码路由显然可行(在映射或页面指令中),但这不可扩展且容易出错。

Possible solution?可能的解决方案?

Using IPageRouteModelConvention seems like the "correct" way of configuring Razor Pages routes?使用 IPageRouteModelConvention 似乎是配置 Razor 页面路由的“正确”方式?

Seems like I can modify the route selectors to strip off the tenant Id sub-dir and therefore make the pages available at the root path.似乎我可以修改路由选择器以剥离租户 Id 子目录,从而使页面在根路径上可用。 However then I also need to make sure the appropriate tenant's page is requested rather than a different tenant's...但是,我还需要确保请求的是适当的租户页面,而不是其他租户的...

One way (I think) this could be done is using an ActionConstraint (which can also be configured in IPageRouteModelConvention).一种方法(我认为)可以做到这一点是使用 ActionConstraint(也可以在 IPageRouteModelConvention 中配置)。 If the origin:tenantId dictionary was hard-coded then I think that would be easy... but my tenant lookup data needs to be pulled from the DB (I actually have a TenantCollection service added as a singleton in the .NET Core service collection already).如果 origin:tenantId 字典是硬编码的,那么我认为这很容易......但是我的租户查找数据需要从数据库中提取(我实际上已经在 .NET 核心服务集合中添加了一个 TenantCollection 服务作为 singleton ).

The problem is that I don't have access to the ServiceProvider (to get my TenantCollection) at builder.Services.Configure(...) call.问题是我无法在 builder.Services.Configure(...) 调用时访问 ServiceProvider(以获取我的 TenantCollection)。 So I can't create the ActionConstraint to restrict access to certain pages for certain origins since I don't have the tenant mapping data.所以我无法创建 ActionConstraint 来限制对某些来源的某些页面的访问,因为我没有租户映射数据。

Here is some example code in-case it helps to illustrate...这是一些示例代码,以防它有助于说明......

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));
});

I feel like I'm missing something obvious, or attacking the problem from the wrong direction?我觉得我错过了一些明显的东西,或者从错误的方向攻击问题?

So, in the end I was "missing something obvious".所以,最后我“遗漏了一些明显的东西”。 In the ActionConstraint the ServiceProvider can be accessed via the ActionConstraintContext's RouteContext.HttpContext.RequestServices reference.在 ActionConstraint 中,可以通过 ActionConstraintContext 的 RouteContext.HttpContext.RequestServices 引用访问 ServiceProvider。 This allows me to get the service I needed to do what I needed.这使我能够获得所需的服务来完成所需的工作。 Simple.简单的。

Instead of leaving it at that, I figure I might as well make this post more worth while.. so I'll give a stripped down implementation of what I'm doing, just in case some future person finds it useful.与其就此打住,我想我还不如让这篇文章更有价值……所以我将给出我正在做的事情的精简实现,以防将来有人发现它有用。

Program.cs程序.cs

...
builder.Services.AddSingleton<MyTenantCollection>();
builder.Services.AddScoped(MyTenant.ImplementationFactoryBasedOnRequestOrigin);

builder.Services.Configure<RazorPagesOptions>(options =>
{
   options.Conventions.Add(new MyPageRouteModelConvention());
});
...

MyPageRouteModelConvention.cs 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 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;
    }
}
...
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();

I have a test service which will return a string.我有一个测试服务,它将返回一个字符串。 then this code worked for me, I can call this service by the code above.然后这段代码对我有用,我可以通过上面的代码调用这个服务。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 多租户:每个租户的单个数据库 - Multi-tenancy: Individual database per tenant 多租户系统中租户控制队列并发 - Control queue concurrency by tenant in multi-tenancy system 具有过滤的dbContext的多租户Web应用程序 - Multi-tenancy web application with filtered dbContext 虚拟导航属性和多租户 - Virtual Navigation Properties and Multi-Tenancy 实体框架多租户和动态模式 - Entity Framework Multi-Tenancy and dynamic schema 具有ASP.NET身份的实体框架多租户 - Entity Framework Multi-Tenancy with ASP.NET Identity 使用 Azure 服务总线重新总线、竞争消费者和多租户 - Rebus with Azure Service Bus, Competing consumers and multi-tenancy 在运行时从子域设置EF ConnectionString以进行多租户设置 - Set EF ConnectionString at runtime from subdomain for multi-tenancy setup EF6 中的多租户具有多个具有相同表的架构 - Multi-tenancy in EF6 with multiple schemas having the same tables 将NHibernate繁重的应用程序从具有多个模式的单租户迁移到多租户 - Moving an NHibernate-heavy application from single-tenancy to multi-tenancy with multiple schemas
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM