正如我们所看到的,多租户的主要前提是我们可以对不同的租户做出不同的响应。你可能会问自己:asked 如何知道它应该服务于哪个租户的内容?也就是说,客户想联系谁?ASP.NET 怎么知道?
这个问题可能有几种答案:
- 从请求的主机名;例如,如果浏览器试图到达abc.com或xyz.net,如请求 URL 中所述
- 来自一个查询字符串参数,如
http://host.com?tenant=abc.com
- 从始发(客户端)主机 IP 地址
- 从始发客户端的域名
最典型(也是最有用)的用例可能是第一个;你有一个单一的网络服务器(或服务器场),它有几个 DNS 记录( A 或 CNAME )分配给它,它将根据它是如何到达不同的响应,比如说,abc.com和xyz.net。作为开发人员,让我们尝试定义一个通用的契约来回答这个问题。考虑以下方法签名:
代码示例 5
String GetCurrentTenant(RequestContext context)
我们可以把它解释为:“给我一些请求,给我相应租户的名字。”
| | 注意:如果你想知道 DNS A 和 CNAME 记录的区别,可以在这里找到很好的解释。 |
请求上下文类是 T2 系统的一部分。Web.Routing 名称空间,它封装了请求的所有属性和上下文。其 HttpContext 属性允许轻松访问常见的 HTTP 请求 URL、服务器变量、查询字符串参数、cookies 和标头以及路由数据路由信息(如果可用)。我为请求选择了这个类,而不是更明显的类——HttpContext和HttpContextBase——正是因为它方便了对路由数据的访问,以防我们需要它。
| | 注: HttpContextBase 于年推出。NET 3.5 允许轻松嘲讽,因为它不是密封的,而且在 ASP.NET 管道之外。它基本上模仿了 HttpContext 的属性和行为,是密封的。 |
至于返回类型,它将是租户的名称。稍后会有更多。
在。NET 世界中,如果我们要重用这样的方法签名,我们有两个主要选项:
- 定义方法委托
- 定义接口或抽象基类
在我们的例子中,我们将进入一个界面,输入ITenantIdentifierStrategy
:
代码示例 6
public interface ITenantIdentifierStrategy
{
String GetCurrentTenant(RequestContext context);
}
图 6:主机头策略
使用请求的主机名(主机 HTTP 头)作为租户标识符的ITenantIdentifierStrategy
的简单实现可能是您在面向公众的网站中更经常使用的实现。在 HTTP 请求中,具有单个 IP 地址以及多个主机和域名的单个服务器将根据请求的主机来区分租户:
表 HTTP 主机头到租户的映射
HTTP 请求头 | 租借 |
---|---|
HTTP/1.1 主持人:abc.com | abc.com |
HTTP/1.1 主持人:xyz.net | xyz.net |
| | 注:有关主机 HTTP 头的更多信息,请参见 RFC 2616,HTTP 头字段定义。 |
使用主机头作为租户名称的类可能如下所示:
代码示例 7
public class HostHeaderTenantIdentifierStrategy : ITenantIdentifierStrategy
{
public String GetCurrentTenant(RequestContext context)
{
return context.HttpContext.Request.Url.Host.ToLower();
}
}
| | 提示:此代码仅用于演示目的;它没有任何类型的验证,只是以小写形式返回请求的主机名。现实生活中的代码应该稍微复杂一些。 |
图 7:查询字符串策略
我们可能希望使用查询字符串参数来区分租户,比如host.com?租户=abc.com :
表 HTTP 查询字符串到租户的映射
| HTTP 请求 URL | 租借 | | http://host.com?租户=abc.com | abc.com | | http://host.com?租户=xyz.net | xyz.net |
这种策略使得用不同的租户进行测试变得非常容易;不需要配置任何东西——只需在 URL 中传递一个查询字符串参数。
我们可以使用如下的类,它选择Tenant
查询字符串参数,并将其指定为租户名称:
public class QueryStringTenantIdentifierStrategy : ITenantIdentifierStrategy
{
public String GetCurrentTenant(RequestContext context)
{
return (context.HttpContext.Request.QueryString["Tenant"] ?? String.Empty).ToLower();
}
}
| | 提示:即使这种技术起初看起来很有趣,但它真的不适合现实生活场景。考虑一下,对于所有内部链接和回发,您必须确保添加租户查询字符串参数;如果因为任何原因,它丢失了,你就被期望的租户抛弃了。 |
| | 注意:这种模式的变体可以使用 HTTP 变量 PATH_INFO 而不是 QUERY_STRING,但是这会有影响,也就是说,对于 MVC。 |
图 8:源 IP 策略
现在,假设我们想从发起请求的 IP 地址中确定租户的名称。假设位于地址为 200.200.200.0/24 的网络上的用户将被分配租户abc.com,而另一个使用160.160.160.160静态 IP 的用户将获得xyz.net。它变得稍微有点棘手,因为我们需要手动注册这些分配,并且我们需要做一些数学运算来找出一个请求是否与注册的网络地址列表相匹配。
我们有两种可能将网络地址与租户名称相关联:
- 我们使用单一的 IP 地址
- 我们使用一个 IP 网络和一个子网掩码。
比如说:
表 3:源 IP 地址到租户的映射
| 源网络/ IP | 坚持 | | 200.200.200.0/24 (200.200.200.1-200.200.200.254) | abc.com | | 160.160.160.1 | xyz.net |
那个。NET 基类库没有为 IP 网络地址操作提供现成的 API,所以我们必须自己构建。考虑以下辅助方法:
代码示例 8
public static class SubnetMask
{
public static IPAddress CreateByHostBitLength(Int32 hostPartLength)
{
var binaryMask = new Byte[4];
var netPartLength = 32 - hostPartLength;
if (netPartLength < 2)
{
throw new ArgumentException
("Number of hosts is too large for IPv4.");
}
for (var i = 0; i < 4; i++)
{
if (i * 8 + 8 <= netPartLength)
{
binaryMask[i] = (Byte) 255;
}
else if (i * 8 > netPartLength)
{
binaryMask[i] = (Byte) 0;
}
else
{
var oneLength = netPartLength - i * 8;
var binaryDigit = String.Empty
.PadLeft(oneLength, '1').PadRight(8, '0');
binaryMask[i] = Convert.ToByte(binaryDigit, 2);
}
}
return new IPAddress(binaryMask);
}
public static IPAddress CreateByNetBitLength(Int32 netPartLength)
{
var hostPartLength = 32 - netPartLength;
return CreateByHostBitLength(hostPartLength);
}
public static IPAddress CreateByHostNumber(Int32 numberOfHosts)
{
var maxNumber = numberOfHosts + 1;
var b = Convert.ToString(maxNumber, 2);
return CreateByHostBitLength(b.Length);
}
}
public static class IPAddressExtensions
{
public static IPAddress[] ParseIPAddressAndSubnetMask(String ipAddress)
{
var ipParts = ipAddress.Split('/');
var parts = new IPAddress[] { ParseIPAddress(ipParts[0]),
ParseSubnetMask(ipParts[1]) };
return parts;
}
public static IPAddress ParseIPAddress(String ipAddress)
{
return IPAddress.Parse(ipAddress.Split('/').First());
}
public static IPAddress ParseSubnetMask(String ipAddress)
{
var subnetMask = ipAddress.Split('/').Last();
var subnetMaskNumber = 0;
if (!Int32.TryParse(subnetMask, out subnetMaskNumber))
{
return IPAddress.Parse(subnetMask);
}
else
{
return SubnetMask.CreateByNetBitLength(subnetMaskNumber);
}
}
public static IPAddress GetBroadcastAddress(this IPAddress address,
IPAddress subnetMask)
{
var ipAdressBytes = address.GetAddressBytes();
var subnetMaskBytes = subnetMask.GetAddressBytes();
if (ipAdressBytes.Length != subnetMaskBytes.Length)
{
throw new ArgumentException
("Lengths of IP address and subnet mask do not match.");
}
var broadcastAddress = new Byte[ipAdressBytes.Length];
for (var i = 0; i < broadcastAddress.Length; i++)
{
broadcastAddress[i] = (Byte)(ipAdressBytes[i] |
(subnetMaskBytes[i] ^ 255));
}
return new IPAddress(broadcastAddress);
}
public static IPAddress GetNetworkAddress(this IPAddress address,
IPAddress subnetMask)
{
var ipAdressBytes = address.GetAddressBytes();
var subnetMaskBytes = subnetMask.GetAddressBytes();
if (ipAdressBytes.Length != subnetMaskBytes.Length)
{
throw new ArgumentException
("Lengths of IP address and subnet mask do not match.");
}
var broadcastAddress = new Byte[ipAdressBytes.Length];
for (var i = 0; i < broadcastAddress.Length; i++)
{
broadcastAddress[i] = (Byte)(ipAdressBytes[i]
& (subnetMaskBytes[i]));
}
return new IPAddress(broadcastAddress);
}
public static Boolean IsInSameSubnet(this IPAddress address2,
IPAddress address, Int32 hostPartLength)
{
return IsInSameSubnet(address2, address, SubnetMask
.CreateByHostBitLength(hostPartLength));
}
public static Boolean IsInSameSubnet(this IPAddress address2,
IPAddress address, IPAddress subnetMask)
{
var network1 = address.GetNetworkAddress(subnetMask);
var network2 = address2.GetNetworkAddress(subnetMask);
return network1.Equals(network2);
}
}
| | 注:此代码基于公开可用的代码此处为(略有修改)。 |
现在我们可以编写ITenantIdentifierStrategy
的实现,它允许我们将 IP 地址映射到租户名称:
代码示例 9
public class SourceIPTenantIdentifierStrategy : ITenantIdentifierStrategy
{
private readonly Dictionary<Tuple<IPAddress, IPAddress>, String> networks = new Dictionary<Tuple<IPAddress, IPAddress>, String>();
public IPTenantIdentifierStrategy Add(IPAddress ipAddress,
Int32 netmaskBits, String name)
{
return this.Add(ipAddress, SubnetMask.CreateByNetBitLength(
netmaskBits), name);
}
public IPTenantIdentifierStrategy Add(IPAddress ipAddress,
IPAddress netmaskAddress, String name)
{
this.networks
[new Tuple<IPAddress, IPAddress>(ipAddress, netmaskAddress)] = name.ToLower();
return this;
}
public IPTenantIdentifierStrategy Add(IPAddress ipAddress, String name)
{
return this.Add(ipAddress, null, name);
}
public String GetCurrentTenant(RequestContext context)
{
var ip = IPAddress.Parse(context.HttpContext.Request
.UserHostAddress);
foreach (var entry in this.networks)
{
if (entry.Key.Item2 == null)
{
if (ip.Equals(entry.Key.Item1))
{
return entry.Value.ToLower();
}
}
else
{
if (ip.IsInSameSubnet(entry.Key.Item1,
entry.Key.Item2))
{
return entry.Value;
}
}
}
return null;
}
}
请注意,此类不是线程安全的;如果你想这么做,一种可能是使用一个ConcurrentDictionary<TKey,TValue > 而不是一个普通的 Dictionary < TKey,TValue > 。
在我们可以使用iptenantidentifier strategy之前,我们需要注册一些映射:
代码示例 10
var s = new SourceIPTenantIdentifierStrategy();
s.Add(IPAddress.Parse("200.200.200.0", 24), "abc.com");
s.Add(IPAddress.Parse("160.160.160.1"), "xyz.net");
在这个例子中,我们看到租户xyz.net被映射到单个 IP 地址,160.160.160.1,而租户abc.com被映射到一个带有 24 位网络掩码的 200.200.200.0 的网络,这意味着从200.200.200.1到200.200.200.254的所有主机都将被包括在内。
图 9:源域策略
我们可能不知道 IP 地址,而是知道域名;因此,一个基于客户域名的策略就应运而生了。我们希望从请求主机的域名部分获取租户的名称,如下所示:
表 4:将源域名映射到租户
| 源域 | 租借 | | *.some.domain | abc.com | | *.xyz.net | xyz.net |
子域也应该包括在内。以下是这样一个策略的可能实现:
代码示例 11
public class SourceDomainTenantIdentifierStrategy : ITenantIdentifierStrategy
{
private readonly Dictionary<String, String> domains = new
Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
public DomainTenantIdentifierStrategy Add(String domain, String name)
{
this.domains[domain] = name;
return this;
}
public DomainTenantIdentifierStrategy Add(String domain)
{
return this.Add(domain, domain);
}
public String GetCurrentTenant(RequestContext context)
{
var hostName = context.HttpContext.Request.UserHostName;
var domainName = String.Join(".", hostName.Split('.')
.Skip(1)).ToLower();
return this.domains.Where(domain => domain.Key == domainName)
.Select(domain => domain.Value).FirstOrDefault();
}
}
当然,对于 DomainTenantIdentifierStrategy
,我们还需要输入一些映射:
代码示例 12
var s = new SourceDomainTenantIdentifierStrategy();
s.Add("some.domain", "abc.com");
s.Add("xyz.net");
第一个条目将来自 some.domain 域(或的子域)的所有客户端请求映射到名为abc.com的租户。第二个对xyz.net域名做了相同的操作,我们跳过了租户的名字,因为它应该与域名相同。
正如您所看到的,前面两个策略的实现——主机头和查询字符串参数——基本上是无状态和不可变的,所以我们可以在一个众所周知的位置创建每个策略的静态实例,而不是每次都创建新的实例。让我们为此创建一个结构:
代码示例 13
public static class TenantsConfiguration
{
public static class Identifiers
{
public static readonly HostHeaderTenantIdentifierStrategy
HostHeader = new HostHeaderTenantIdentifierStrategy();
public static readonly QueryStringTenantIdentifierStrategy
QueryString = new QueryStringTenantIdentifierStrategy();
}
| | 提示:请注意默认租户属性。如果租户识别策略无法将请求映射到租户,将使用这种方法。 |
另外两种策略——按源 IP 地址和按域名——需要配置,所以我们不应该将它们作为常量实例,但是,为了便于查找,让我们在刚才介绍的TenantsConfiguration
类中添加一些静态工厂:
代码示例 14
public static class TenantsConfiguration
{
//rest goes here
public static class Identifiers
{
//rest goes here
public static SourceDomainTenantIdentifierStrategy SourceDomain()
{
return new SourceDomainTenantIdentifierStrategy();
}
public static SourceIPTenantIdentifierStrategy SourceIP()
{
return new SourceIPTenantIdentifierStrategy();
}
}
}
| | 注:在第 9 章“将所有这些放在一起”中,我们将看到所有这些策略是如何关联的。 |
我们已经研究了从请求中获取租户姓名的一些策略;现在,我们必须挑选一个,并把它存放在容易找到的地方。
一种选择是将其作为静态属性存储在TenantsConfiguration
类中:
代码示例 15
public static class TenantsConfiguration
{
public static ITenantIdentifierStrategy TenantIdentifier { get; set; }
//rest goes here
}
现在,我们可以选择我们想要的任何策略,可能从TenantsConfiguration
的静态成员中选择一个。此外,我们需要设置DefaultTenant
属性,这样如果当前策略无法确定要使用的租户,我们有一个后备方案:
代码示例 16
TenantsConfiguration.TenantIdentifier = TenantsConfiguration.Identifiers.HostHeader;
TenantsConfiguration.DefaultTenant = "abc.com";
另一种选择是使用控制反转 (IoC)框架来存储对我们选择的租户标识符实例的单例引用。更好的是,我们可以使用公共服务定位器来提取我们正在使用的国际奥委会。这样,我们就不会受限于特定的实现,甚至可以在不影响代码的情况下更改要使用的实现(当然,一些引导代码除外)。有几个 IoC 容器用于。NET 框架。接下来,我们将看到一个使用众所周知的 IoC 框架的例子,微软 Unity ,微软企业库的一部分。
代码示例 17
//set up Unity
var unity = new UnityContainer();
//register instances
unity.RegisterInstance<ITenantIdentifierStrategy>(TenantsConfiguration.Identifiers.HostHeader);
unity.RegisterInstance<String>("DefaultTenant", "abc.com");
//set up Common Service Locator with the Unity instance
ServiceLocator.SetLocatorProvider(() => new UnityServiceLocator(unity));
//resolve the tenant identifier strategy and the default tenant
var identifier = ServiceLocator.Current.GetInstance<ITenantIdentifierStrategy>();
var defaultTenant = ServiceLocator.Current.GetInstance<String>("DefaultTenant");
这种方法的优点是,如果需要,我们可以通过代码(或通过 Web.config 文件)动态注册新策略,而无需更改任何代码。我们将在整本书中使用这种方法。第 9 章“应用程序服务”的服务定位器部分也讨论了 Unity 以及如何使用它来返回特定于当前租户的组件。
| | 注意:Unity 只是几十个 IoC 容器中的一个,提供类似的功能和更具体的功能。不是所有的都有公共服务定位器的适配器,但是通常很容易实现一个。有关 IoC、公共服务定位器和 Unity 的更深入讨论,请参见微软 Unity 简洁地。 |
现在我们有了一个抽象,可以给我们一个租户的名字,让我们想一想一个租户还需要什么。
我们可能还需要一个主题,一个集合了配色方案和字体的主题。不同的租户可能想要不同的外观。
在 ASP.NET 世界中,两个主要的框架,MVC 和 Web Forms,提供了母版页或布局页的概念(在 MVC 术语中),用于定义 HTML 页面的全局布局。这样,很容易在整个站点中实施一致的布局,因此,让我们考虑一个母版页(或者用 MVC 术语来说是布局页)属性。
不管有没有更高级的配置功能,拥有一组特定于租户的键/值对都没有坏处,所以我们现在有了一个通用的属性包。
Windows 为监控应用程序提供了一种通用的、操作系统支持的机制:性能计数器。性能计数器允许我们实时监控应用程序的某些方面,甚至自动对条件做出反应。我们将展示一组计数器实例,它们将与租户相关联地自动创建。
提供一个通用的扩展机制可能是有用的;在 IoC 流行之前。NET 已经包含了一个通用接口,用于从类型中解析组件,其形式为iseservice provider接口。让我们也考虑一个使用这个接口的服务解析机制。
最后,让租户在向我们的框架注册时初始化本身是有意义的。这不是数据,而是行为。
因此,基于我们所讨论的内容,我们的租户定义界面ITenantConfiguration
将如下所示:
代码示例 18
public interface ITenantConfiguration
{
String Name { get; }
String Theme { get; }
String MasterPage { get; }
IServiceProvider ServiceProvider { get; }
IDictionary<String, Object> Properties { get; }
IEnumerable<String> Counters { get; }
void Initialize();
}
例如,名为xyz.net
的租户可能具有以下配置:
代码示例 19
public sealed class XyzNetTenantConfiguration : ITenantConfiguration
{
public XyzNetTenantConfiguration()
{
//do something productive
this.Properties = new Dictionary<String, Object>();
this.Counters = new List<String> { "C", "D", "E" };
}
public void Initialize()
{
//do something productive
}
public String Name { get { return "xyz.net"; } }
public String MasterPage { get { return this.Name; } }
public String Theme { get { return this.Name; } }
public IServiceProvider ServiceProvider { get; private set; }
public IDictionary<String, Object> Properties { get; private set; }
public IEnumerable<String> Counters { get; private set; }
}
在这个例子中,我们返回了一个与租户的Name
相同的MasterPage
和一个Theme
,并且没有返回在Counters
、Properties
和ServiceProvider
属性中真正有用的东西,但是在现实生活中,您可能会做一些其他的事情。您返回的任何计数器名称都将自动创建为数字性能计数器实例。
| | 注意:有许多其他选项可以提供这类信息,例如属性。 |
没有人居住的房子是什么?
现在,我们必须想出一个寻找租户的策略。我想到了两个基本方法:
- 显式手动配置单个租户
- 自动查找和注册租户
同样,让我们在一个漂亮的界面中抽象这个功能,ITenantLocationStrategy
:
代码示例 20
public interface ITenantLocationStrategy
{
IDictionary<String, Type> GetTenants();
}
该接口返回名称为和的类型的集合,其中类型是实现ITenantConfiguration
的某个非抽象类的实例,而名称是唯一的租户标识符。
我们还将在TenantsConfiguration
、DefaultTenant
中保留一个静态属性,在这里我们可以存储默认租户的名称,作为无法自动识别的后备:
代码示例 21
public static class TenantsConfiguration
{
public static String DefaultTenant { get; set; }
//rest goes here
}
以下是定位和识别租户的策略:
代码示例 22
public static class TenantsConfiguration
{
public static String DefaultTenant { get; set; }
public static ITenantIdentifierStrategy TenantIdentifier { get; set; }
public static ITenantLocationStrategy TenantLocation { get; set; }
//rest goes here
}
接下来,我们将看到一些注册租户的方法。
也许注册租户最明显的方法是在 Web.config 文件中有一个配置部分,我们可以在其中列出实现租户配置的所有类型。我们希望有一个简单的结构,像这样:
代码示例 23
<tenants default="abc.com">
<add name="abc.com" type="MyNamespace.AbcComTenantConfiguration, MyAssembly" />
<add name="xyz.net" type="MyNamespace.XyzNetTenantConfiguration, MyAssembly" />
</tenants>
在tenants
部分,我们有许多包含以下属性的元素:
Name
:唯一的租户标识符;在这种情况下,它将与我们希望回答的域名相同(参见主机头策略)Type
:实现ITenantConfiguration
的类的全限定类型名Default
:默认租户,如果从当前租户标识符策略中无法识别租户
中相应的配置类。NET 是:
代码示例 24
[Serializable]
public class TenantsSection : ConfigurationSection
{ public static readonly TenantsSection Section = ConfigurationManager
.GetSection("tenants") as TenantsSection;
[ConfigurationProperty("", IsDefaultCollection = true, IsRequired = true)]
public TenantsElementCollection Tenants
{
get
{
return base[String.Empty] as TenantsElementCollection;
}
}
}
[Serializable]
public class TenantsElementCollection : ConfigurationElementCollection,
IEnumerable<TenantElement>
{
protected override String ElementName { get { return String.Empty; } }
protected override ConfigurationElement CreateNewElement()
{
return new TenantElement();
}
protected override Object GetElementKey(ConfigurationElement element)
{
var elm = element as TenantElement;
return String.Concat(elm.Name, ":", elm.Type);
}
IEnumerator<TenantElement> IEnumerable<TenantElement>.GetEnumerator()
{
foreach (var elm in this.OfType<TenantElement>())
{
yield return elm;
}
}
}
[Serializable]
public class TenantElement : ConfigurationElement
{
[ConfigurationProperty("name", IsKey = true, IsRequired = true)]
[StringValidator(MinLength = 2)]
public String Name
{
get { return this["name"] as String; }
set { this["name"] = value; }
}
[ConfigurationProperty("type", IsKey = true, IsRequired = true)]
[TypeConverter(typeof(TypeTypeConverter))]
public Type Type
{
get { return this["type"] as Type; }
set { this["type"] = value; }
}
[ConfigurationProperty("default", IsKey = false, IsRequired = false,
DefaultValue = false)]
public Boolean Default
{
get { return (Boolean)(this["default"] ?? false); }
set { this["default"] = value; }
}
}
因此,我们的租户位置策略( ITenantLocationStrategy
)实现可能类似于以下内容:
代码示例 25
public sealed class XmlTenantLocationStrategy : ITenantLocationStrategy
{
public static readonly ITenantLocationStrategy Instance = new XmlTenantLocationStrategy();
public IDictionary<String, Type> GetTenants()
{
var tenants = TenantsSection.Section.Tenants
.ToDictionary(x => x.Name, x => x.Type);
foreach (var tenant in TenantsSection.Section.Tenants
.OfType<TenantElement>())
{
if (tenant.Default)
{
if (String.IsNullOrWhiteSpace
(TenantsConfiguration.DefaultTenant))
{
TenantsConfiguration.DefaultTenant =
tenant.Name;
}
}
}
return tenants;
}
}
你可能已经注意到Instance
字段;因为拥有这个类的几个实例没有多大意义,因为它们都指向同一个。配置文件,我们可以有它的一个静态实例,并总是在必要时使用它。现在,我们所要做的就是将这个策略设置为在TenantsConfiguration
中使用的策略。如果我们要使用公共服务定位器策略(参见 Unity 和公共服务定位器),我们需要在 Unity 注册方法和TenantsConfiguration
静态类中添加以下行:
代码示例 26
public static class TenantsConfiguration
{
//rest goes here
public static class Locations
{
public static XmlTenantLocationStrategy Xml()
{
return XmlTenantLocationStrategy.Instance;
}
//rest goes here
}
}
container.RegisterInstance<ITenantLocationStrategy>(TenantsConfiguration.Locations. Xml());
通过在TenantsConfiguration
类中保留所有策略的工厂,开发人员可以更容易地从开箱即用的策略集中找到他们需要的策略。
至于自动寻找租户,有几种选择,但我选择使用微软可扩展性框架 (MEF)。这是附带的框架。NET,它提供了从文件系统中自动定位插件的机制。其概念包括:
- 约定类型:描述要导入的功能的抽象基类或接口(插件 API)
- 合同名称:用于区分同一合同类型多个部分的自由格式名称
- 部分:导出特定合同类型和名称的具体类,可在目录中找到
- 目录:实现寻找零件策略的 MEF 类
图 10: MEF 架构
图 11: MEF 零件
我们不会深入探讨 MEF 我们将看到如何使用它在代码中自动查找租户配置类。我们只需要用几个属性来修饰插件类——在我们的例子中是租户配置——选择一个要使用的策略,MEF 就会为我们找到并可选地实例化它们。让我们看一个在租户配置类中使用 MEF 属性的例子:
代码示例 27
[ExportMetadata("Default", true)]
[PartCreationPolicy(CreationPolicy.Shared)]
[Export("xyz.net", typeof(ITenantConfiguration))]
public sealed class XyzNetTenantConfiguration : ITenantConfiguration
{
public XyzNetTenantConfiguration()
{
//do something productive
this.Properties = new Dictionary<String, Object>();
this.Counters = new List<String> { "C", "D", "E" };
}
public void Initialize()
{
//do something productive
}
public String Name { get { return "xyz.net"; } }
public String MasterPage { get { return this.Name; } }
public String Theme { get { return this.Name; } }
public IServiceProvider ServiceProvider { get; private set; }
public IDictionary<String, Object> Properties { get; private set; }
public IEnumerable<String> Counters { get; private set; }
}
这个类基本上与代码示例 18 中的类相同,只是增加了 ExportMetadataAttribute 、PartCreationPolicyAttribute和 ExportAttribute 属性。这些是 MEF 框架的一部分,其目的是:
- ExportMetadataAttribute :允许向注册添加自定义属性;您可以根据需要添加任意多个。这些属性对 MEF 没有任何特殊意义,只会对导入插件的类有意义。
- PartCreationPolicyAttribute:这个插件将如何被 MEF 创建;目前有两种选择:共享(对于单身者)或非共享(对于短暂实例,默认)
- 导出属性:将导出的类型标记为零件。这是唯一必需的属性。
现在,我们使用 MEF 实施的租户位置策略是这样的:
代码示例 28
public sealed class MefTenantLocationStrategy : ITenantLocationStrategy
{
private readonly ComposablePartCatalog catalog;
public MefTenantLocationStrategy(params String [] paths)
{
this.catalog = new AggregateCatalog(paths.Select(
path => new DirectoryCatalog(path)));
}
public MefTenantLocationStrategy(params Assembly [] assemblies)
{
this.catalog = new AggregateCatalog(assemblies
.Select(asm => new AssemblyCatalog(asm)));
}
public IDictionary<String, Type> GetTenants()
{
//get the default tenant
var tenants = this.catalog.GetExports(
new ImportDefinition(a => true, null,
ImportCardinality.ZeroOrMore, false, false))
.ToList();
var defaultTenant = tenants.SingleOrDefault(x => x.Item2.Metadata
.ContainsKey("Default"));
if (defaultTenant != null)
{
var isDefault = Convert.ToBoolean(defaultTenant.Item2
.Metadata["Default"]);
if (isDefault)
{
if (String.IsNullOrWhiteSpace(
TenantsConfiguration.DefaultTenant))
{
TenantsConfiguration.DefaultTenant =
defaultTenant.Item2.ContractName;
}
}
}
return this.catalog.GetExportedTypes<ITenantConfiguration>();
}
}
这段代码将从一组程序集或一组路径中查找零件。然后,如果没有设置默认值,它将尝试将租户设置为默认租户。让我们将其添加到TenantsConfiguration
类中:
代码示例 29
public static class TenantsConfiguration
{
//rest goes here
public static class Locations
{
public static XmlTenantLocationStrategy Xml()
{
return XmlTenantLocationStrategy.Instance;
}
public static MefTenantLocationStrategy Mef
(params Assembly[] assemblies)
{
return new MefTenantLocationStrategy(assemblies);
}
public static MefTenantLocationStrategy Mef(params String [] paths)
{
return new MefTenantLocationStrategy(paths);
}
//rest goes here
}
}
container.RegisterInstance<ITenantLocationStrategy>(TenantsConfiguration.Locations. Mef("some", "path");
最后,我们将此实现注册为默认的租户位置策略—请记住,只能有一个。这将是ITenantLocationStrategy
类型的公共服务定位器返回的实例。
我们需要在 web 应用程序开始时运行引导代码。我们有几个选择:
- 在应用程序的应用程序 _ 开始事件中,通常在 Global.asax.cs 中定义
- 使用预应用程序开始方法属性,在应用程序开始之前运行代码
在任何情况下,我们都需要设置策略并获取租户列表:
代码示例 30
TenantsConfiguration.DefaultTenant = "abc.com";
TenantsConfiguration.TenantIdentifier = TenantsConfiguration.Identifiers
.HostHeader;
TenantsConfiguration.TenantLocation = TenantsConfiguration.Locations.Mef();
TenantsConfiguration.Initialize();
这里有一个更新的TenantsConfiguration
类:
代码示例 31
public static class TenantsConfiguration
{
public static String DefaultTenant { get; set; }
public static ITenantIdentifierStrategy TenantIdentifierStrategy
{ get; set; }
public static ITenantLocationStrategy TenantLocationStrategy { get; set; }
public static void Initialize()
{
var tenants = GetTenants();
InitializeTenants(tenants);
CreateLogFactories(tenants);
CreatePerformanceCounters(tenants);
}
private static void InitializeTenants
(IEnumerable<ITenantConfiguration> tenants)
{
foreach (var tenant in tenants)
{
tenant.Initialize();
}
}
private static void CreatePerformanceCounters(
IEnumerable<ITenantConfiguration> tenants)
{
if (PerformanceCounterCategory.Exists("Tenants") == false)
{
var col = new CounterCreationDataCollection(tenants
.Select(t => new CounterCreationData(t.Name,
String.Empty, PerformanceCounterType.NumberOfItems32))
.ToArray());
var category = PerformanceCounterCategory
.Create("Tenants",
"Tenants Performance Counters",
PerformanceCounterCategoryType
.MultiInstance,
col);
foreach (var tenant in tenants)
{
foreach (var instanceName in tenant.Counters)
{
using (var pc = new PerformanceCounter(
category.CategoryName,
tenant.Name,
String.Concat(tenant.Name,
":",
instanceName), false))
{
pc.RawValue = 0;
}
}
}
}
}
}
通过利用ServiceProvider
属性,我们可以添加自己的特定于租户的服务。例如,考虑这个实现,它注册了一个私有的 Unity 容器,并向其中添加了几个服务:
代码示例 32
public sealed class XyzNetTenantConfiguration : ITenantConfiguration
{
private readonly IUnityContainer container = new UnityContainer();
public void Initialize()
{
this.container.RegisterType<IMyService, MyServiceImpl>();
this.container.RegisterInstance<IMyOtherService>(new MyOtherServiceImpl());
}
public IServiceProvider ServiceProvider { get { return this.container; } }
//rest goes here
}
然后,我们可以从当前租户那里获得实际服务(如果有):
代码示例 33
var tenant = TenantsConfiguration.GetCurrentTenant();
var myService = tenant.ServiceProvider.GetService(typeof(IMyService)) as IMyService;
这里有一个小问题:如果没有给定类型的注册,Unity 将抛出一个异常。为了解决这个问题,我们可以在 IServiceProvider 上使用这个不错的扩展方法:
代码示例 34
public static class ServiceProviderExtensions
{
public static T GetService<T>(this IServiceProvider serviceProvider)
{
var service = default(T);
try
{
service = (T) serviceProvider.GetService(typeof (T));
}
catch
{
}
return service;
}
}
如您所见,除了执行强制转换,如果不存在注册,它还会返回泛型参数的默认类型(在接口的情况下,可能是 null
)。