ASP.NET MVC引入了非常不同的开发模式。大多数人会同意,它更具可测试性,促进了职责分离——用户界面的视图,业务逻辑的控制器——并且更接近于 HTTP 协议,避免了 Web Forms 使用的一些魔法,这些魔法虽然有用,但会导致复杂性增加和性能下降。
注:Nick Harrison 简洁地为简洁系列写了MVC;请务必阅读它,以获得对 MVC 的良好介绍。
在这里,我们将探索 MVC 提供的三种品牌机制:
- 页面布局:相当于网页表单的主页,我们将为每个租户提供不同的主页。
- 视图位置:每个租户都将其视图存储在特定的文件夹中。
- CSS 捆绑包:每个租户都有自己的 CSS 捆绑包(特定于租户的集合)。css 文件)。
MVC 的视图有一个类似于 Web Forms 母版页的机制,名为页面布局。页面布局指定了 HTML 内容的全局结构,留下“漏洞”供使用它的页面填充。默认情况下,ASP.NET MVC 对 C#视图使用**" ~/view/Shared/_ layout . cshtml "**布局,对 Visual Basic 视图使用 "_Layout.vbhtml" 。
使用页面布局的唯一方法是在视图中显式设置它,如本例中,使用 Razor :
代码示例 52
@{
Layout = "~/Views/Shared/Layout.cshtml";
}
为了避免仅仅为了设置页面布局而一遍又一遍地重复代码,我们可以使用 MVC 的扩展性点之一视图引擎。因为我们将使用 Razor 作为首选的视图引擎,我们需要子类 RazorViewEngine ,以便注入我们特定于租户的页面布局。
我们的实现如下所示:
代码示例 53
public sealed class MultitenantRazorViewEngine : RazorViewEngine
{
public override ViewEngineResult FindView(ControllerContext ctx,
String viewName, String masterName, Boolean useCache)
{
var tenant = TenantsConfiguration.GetCurrentTenant();
//the third parameter to FindView is the page layout
return base.FindView(controllerContext, viewName,
tenant.MasterPage, useCache);
}
}
为了使用我们的新视图引擎,我们需要在视图引擎中替换现有的视图引擎。发动机集合;这应该通过从应用程序 _ 开始派生的方法来完成:
代码示例 54
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new MultitenantRazorViewEngine());
最后,我们必须确保我们的视图从 ViewBag.Layout
设置页面布局(布局属性),该属性映射到 FindView 的第三个参数中返回的内容:
代码示例 55
@{
Layout = ViewBag.Layout;
}
默认情况下,视图引擎将查找视图()。cshtml 或**。vbhtml** )在一个预定义的虚拟路径集合中。我们想做的是首先在特定于租户的路径中寻找一个视图,如果在那里找不到,就返回到默认位置。我们为什么要那样?通过这种方式,我们可以将视图设计得更具有特定于租户的外观,而不仅仅是改变页面布局。
我们将使用上一页中介绍的相同视图引擎,并为此目的对其进行调整:
代码示例 56
public sealed class MultitenantRazorViewEngine : RazorViewEngine
{
private Boolean pathsSet = false;
public MultitenantRazorViewEngine() : this(false) { }
public MultitenantRazorViewEngine(Boolean usePhysicalPathsPerTenant)
{
this.UsePhysicalPathsPerTenant = usePhysicalPathsPerTenant;
}
public Boolean UsePhysicalPathsPerTenant { get; private set; }
private void SetPaths(ITenantConfiguration tenant)
{
if (this.UsePhysicalPathsPerTenant)
{
if (!this.pathsSet)
{
this.ViewLocationFormats = new String[]
{
String.Concat("~/Views/",
tenant.Name, "/{1}/{0}.cshtml"),
String.Concat("~/Views/",
tenant.Name, "/{1}/{0}.vbhtml")
} .Concat(this.ViewLocationFormats).ToArray();
this.pathsSet = true;
}
}
}
public override ViewEngineResult FindView(ControllerContext ctx,
String viewName, String masterName, Boolean useCache)
{
var tenant = TenantsConfiguration.GetCurrentTenant();
//the third parameter to FindView is the page layout
return base.FindView(controllerContext, viewName,
tenant.MasterPage, useCache);
}
}
这要求视图引擎配置有 usePhysicalPathsPerTenant
设置:
代码示例 57
ViewEngines.Engines.Add(new MultitenantRazorViewEngine(true));
CSS 捆绑和缩小集成在 ASP 的两大口味中。网络表单和 MVC。在网络表单中,因为它有主题机制(参见主题和皮肤),它可能不太用于品牌化,但是 MVC 没有类似的机制。
一个 CSS 包由一个名称和一组组成。css 文件位于任意数量的文件夹中。我们将为每个注册的租户创建一个名称相同的包,该包包含租户配置的 ~/Content 文件夹下的 Theme
属性中命名的文件夹中的所有文件。以下是如何:
代码示例 58
public static void RegisterBundles(BundleCollection bundles)
{
foreach (var tenant in TenantsConfiguration.GetTenants())
{
var virtualPath = String.Format("~/{0}", tenant.Name);
var physicalPath = String.Format("~/Content/{0}",
tenant.Theme);
if (!BundleTable.Bundles.Any(b => b.Path == virtualPath))
{
var bundle = new StyleBundle(virtualPath)
.IncludeDirectory(physicalPath, "*.css");
BundleTable.Bundles.Add(bundle);
}
}
}
注册租户后需要调用此方法:
代码示例 59
RegisterBundles(BundleTable.Bundles);
租户**“ABC . com”将获得一个名为“/ABC . com”包含所有的 CSS 包。**“/Content/ABC . com”下的 css 文件。
然而,这还不是全部;为了将 CSS 包实际添加到视图中,我们需要在视图上添加一个显式调用,如下所示:
代码示例 60
@Styles.Render("~/abc.com")
然而,如果我们要硬编码租户的名字,这将无法扩展。这对于特定于租户的布局页面来说是可以的,但是对于通用视图来说就不行了。我们需要的是一种机制,可以根据每个请求自动提供正确的包名。幸运的是,我们可以通过一个动作过滤器来实现这一点:
代码示例 61
public sealed class MultitenantActionFilter : IActionFilter
{
void IActionFilter.OnActionExecuted(ActionExecutedContext ctx) { }
void IActionFilter.OnActionExecuting(ActionExecutingContext ctx)
{
var tenant = TenantsConfiguration.GetCurrentTenant();
ctx.Controller.ViewBag.Tenant = tenant.Name;
ctx.Controller.ViewBag.TenantCSS = String.Concat("~/",
filterContext.Controller.ViewBag.Tenant);
}
}
这个动作过滤器实现了 IActionFilter 界面,它所做的只是在viewpag集合中注入两个值,租户名称( Tenant
)和 CSS 包的名称( TenantCSS
)。动作过滤器可以通过多种方式注册,其中之一是全局过滤器。过滤器集合:
代码示例 62
GlobalFilters.Filters.Add(new MultitenantActionFilter());
打开此选项后,我们只需要在视图上添加自定义的特定于租户的捆绑包:
代码示例 63
@Styles.Render(ViewBag.TenantCSS)
我们可能希望限制某些租户的某些控制器操作。MVC 提供了一个名为授权过滤器的钩子,可以用来允许或拒绝对给定控制器或动作方法的访问。有一个基本实现, AuthorizeAttribute ,它只在当前用户通过身份验证时才授予访问权限。实现允许可扩展性,这就是我们要做的:
代码示例 64
[Serializable]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
AllowMultiple = false, Inherited = true)]
public sealed class AllowedTenantsAttribute : AuthorizeAttribute
{
public AllowedTenantsAttribute(params String [] tenants)
{
this.Tenants = tenants;
}
public IEnumerable<String> Tenants { get; private set; }
protected override Boolean AuthorizeCore(HttpContextBase ctx)
{
var tenant = TenantsConfiguration.GetCurrentTenant();
return this.Tenants.Any(x => x == tenant.Name);
}
}
当应用于以一个或多个租户为参数的控制器方法或类时,它将只授予指定租户访问权限:
代码示例 65
public class SomeController : Controller
{
[AllowedTenants("abc.com")]
public ActionResult SomeThing()
{
//this can only be accessed by abc.com
//return something
}
[AllowedTenants("xyz.net", "abc.com")]
public ActionResult OtherThing()
{
//this can be accessed by abc.com and xyz.net
//return something
}
}
下一章描述了另一种无需代码就能限制资源访问的技术。
测试 MVC 控制器和动作很简单。在多租户应用程序的环境中,正如我们在本书中所讨论的,我们只需要根据正确的租户识别策略来设置我们的测试平台。例如,如果你正在使用 NUnit ,你会在每次测试之前使用一个用 SetUpFixtureAttribute 修饰的方法来完成:
代码示例 66
[SetUpFixture]
public static class MultitenantSetup
{
[SetUp]
public static void Setup()
{
var req = new HttpRequest(
filename: String.Empty,
url: "http://abc.com/",
queryString: String.Empty);
req.Headers["HTTP_HOST"] = "abc.com";
//add others as needed
var response = new StringBuilder();
var res = new HttpResponse(new StringWriter(response));
var ctx = new HttpContext(req, res);
var principal = new GenericPrincipal(
new GenericIdentity("Username"), new [] { "Role" });
var culture = new CultureInfo("pt-PT");
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
HttpContext.Current = ctx;
HttpContext.Current.User = principal;
}
}