ASP.NET 2.0 Provider 模型,一

介绍:

ASP.NET 2.0包含了将状态存储在数据库或者其他存储介质中的一些服务。例如,会话状态服务通过将每个用户的会话状态存储在进程中(在主机应用程序池的应用程序内存中),外部进程内存中(状态服务进程),或者在Microsoft SQL Server数据库中,而成员资格服务将用户姓名,密码和其他在Microsoft SQL Server数据库中数据存储在活动目录中。对大多数应用程序来说,内置的存储选择已经是足够了。但是,有时会需要将状态存储在其他的介质中,如Oracle数据库、DB2数据库、自定义架构的Microsoft SQL Server数据库、XML文件,甚至是通过Web Service提供的数据源。

在ASP.NET 1.x中,开发人员如果希望将状态存储在其他可选介质中往往需要面对重写ASP.NET大部分代码所带来的繁重工作量。与之形成对比的是,ASP.NET 2.0,给状态管理引入了强大的伸缩性。ASP.NET状态管理服务并不是直接与存储介质打交道,而是使用providers作为中间媒介,如图1所示。

         图1 ASP.NET 2.0 provider模型

provider是一个在服务和数据源之间提供统一接口的软件模块。Providers抽象了物理存储介质,类似于设备驱动程序抽象物理硬件设备。因为实际上的所有的ASP.NET 2.0状态管理服务都是基于provider的,因此存储会话状态或者成员资格状态在Oracle数据库中而不是在Microsoft SQL数据库中跟添加一个Oracle会话状态provider或成员资格provider一样简单。在provider之外的代码都不需要被修改,一个简单的配置改变,通过Web.config来完成,将相关的服务连接到Oracle providers

由于有了provider模型,ASP.NET 2.0实际上通过配置可以在任何地方存储状态。例如,成员资格数据,从Web service中取得与从数据库中取得同样简单。仅仅是通过一个自定义的provider。一些公司喜欢通过第三方获得自定义providers。此外,尽管如果,想写他们自己的,或者是因为没有不经过定制就适合的provider,或者是因为他们希望让ASP.NET适应已经在使用的存储介质(例如,已经存在的成员资格管理数据库)。本文阐述了ASP.NET 2.0 provider模型并提供了开发者写出充满活力的高质量的providers。

Provider模型的目标:

ASP.NET 2.0 Provider模型出于以下的目标而设计的:

· 让ASP.NET状态存储既具有灵活性又具有可扩展性。

· 将应用程序层代码与在存储状态的介质中的运行时代码分离,以及将要求在一个简单的友好的界面下选择其他介质所带来的变化分离。

· 通过提供一组充满活力的具有良好书写格式的基类,开发者可以通过继承这些类来创建他们自己的provider类,这让创建自定义providers非常的简单。

我们希望,因为没有不加定制就可以使用的providers,希望将ASP.NET 2.0与数据源配对使用的开发者们,能够通过适当的努力,创建自定义的providers来做这个工作。

Provider模型

图2描述了应用于ASP.NET 成员资格服务的provider模型。在最上一层是login控件:Login,LoginView,以及其他为用户登录提供的统一接口,如找回失去的密码以及更多的。在login控件之下运行的是成员资格服务,它提供了控件使用的公共API函数,这些函数同样也可以被应用程序代码使用。成员资格服务在其数据源中存储了登录信任及其他的信息。它不是直接访问数据源,而是与成员资格provider结合起来。因此,login控件和成员资格服务本身都能通过添加新的providers来适应不同类型的数据源(例如Oracle数据库)。

图2 成员资格provider模型

成员资格providers实现了一个叫做MembershipProvider的抽象类,它是包含了方法和属性的一个良好定义的接口。因为所有的成员资格providers都是建立在统一的协议上,所以成员资格服务与他们结合的时候根本不需要知道或关心providers是怎么存储数据的。

Provider类型

成员资格是使用provider架构的ASP.NET 2.0服务之一。表1列举了基于provider的功能和服务及向他们提供服务的缺省providers。

表1 基于provider的服务

特征和服务

缺省的provider

Membership

System.Web.Security.SqlMembershipProvider

Role management

System.Web.Security.SqlRoleProvider

Site map

System.Web.XmlSiteMapProvider

Profile

System.Web.Profile.SqlProfileProvider

Session state

System.Web.SessionState.InProcSessionStateStore

Web events

N/A (see below)

Web Parts personalization

System.Web.UI.WebControls.WebParts.SqlPersonalizationProvider

Protected configuration

N/A(see below)

SQL providers 例如SqlMembershipProvider和SqlProfileProvider,使用Microsoft SQL Server或者SQL Server Express作为它们的数据源。InProcSessionStateStore在内存中存储会话状态,而XmlSiteMapProvider实用XML文件作为它的数据源。N/A(没有可用的)表示特征和服务需要必须被明确识别的providers。包括:

· Web events,必须被明确的映射到具有<healthMonitoring>结构节的providers。在最主要的Web.config中将特定的Web events映射到System.Web.Management.EventLogWebEventProvider,这样使它们不用经过任何步骤就能连接到Windows事件日志。

· Protected configuration,要求调用它的加密服务的调用者指定一个provider。ASP.NET 自带的Aspnet_regiis.exe工具在加密和解密结构节中使用protected configuration。除非被指定以另外一种方式来做,Aspnet_regiis.exe调用System.Configuration.RsaProtectedConfigurationProvider来提供加密和解密服务。

内置providers

表2列举了ASP.NET 2.0内置的providers。

表2 ASP.NET 2.0 providers

Provider类型

内置的Provider(s)

Membership

System.Web.Security.ActiveDirectoryMembershipProvider

System.Web.Security.SqlMembershipProvider

Role management

System.Web.Security.AuthorizationStoreRoleProvider

System.Web.Security.SqlRoleProvider

System.Web.Security.WindowsTokenRoleProvider

Site map

System.Web.XmlSiteMapProvider

Profile

System.Web.Profile.SqlProfileProvider

Session state

System.Web.SessionState.InProcSessionStateStore

System.Web.SessionState.OutOfProcSessionStateStore

System.Web.SessionState.SqlSessionStateStore

Web events

System.Web.Management.EventLogWebEventProvider

System.Web.Management.SimpleMailWebEventProvider

System.Web.Management.TemplatedMailWebEventProvider

System.Web.Management.SqlWebEventProvider

System.Web.Management.TraceWebEventProvider

System.Web.Management.WmiWebEventProvider

Web Parts

personalization

System.Web.UI.WebControls.WebParts.

SqlPersonalizationProvider

Protected configuration

System.Configuration.DPAPIProtectedConfigurationProvider

System.Configuration.RSAProtectedConfigurationProvider

此外,Microsoft打算将一些用于Microsoft Access的providers免费下载。开发者不被鼓励使用Microsoft Access作为企业应用程序的后端。但是Microsoft也认识到,Access可能会适合拥有有限用户的小的web站点。

Provider基类

System.Configuration.Provider命名空间包含了一个叫做ProviderBase的类,它是为所有providers提供服务的根类。ProviderBase如下定义:

public class ProviderBase

{

public virtual string Name { get; }

public virtual string Description { get; }

public virtual void Initialize (string name,

NameValueCollection config);

}

Name属性返回了provider的名称(例如”AspNetSqlMembershipProvider”),而Description返回了一个原文的描述。Initialize当provider被装载的时候被ASP.NET调用,给与provider时机来初始化自己。Name参数包含着provider的名字;它的值来自注册provider的<add>元素的name 属性,如下:

<add name="AspNetSqlMembershipProvider" ... />

Config参数出现在<add>元素包含着剩余的name/value对。

Initialize的默认实现方式要确保Initialize在之前没有被调用过,并以同样名字的配置属性来初始化Name和Description。ProviderBase的源代码如下实现:

namespace System.Configuration.Provider

{

using System.Collections.Specialized;

using System.Runtime.Serialization;

public abstract class ProviderBase

{

private string _name;

private string _Description;

private bool _Initialized;

public virtual string Name { get { return _name; } }

public virtual string Description

{

get { return string.IsNullOrEmpty(_Description) ?

Name : _Description; }

}

public virtual void Initialize(string name,

NameValueCollection config)

{

lock (this) {

if (_Initialized)

throw new InvalidOperationException("...");

_Initialized = true;

}

if (name == null)

throw new ArgumentNullException("name");

if (name.Length == 0)

throw new ArgumentException("...", "name");

_name = name;

if (config != null) {

_Description = config["description"];

config.Remove("description");

}

}

}

}

如果开发者想要写自定义基于provider的services(查看“自定义基于Provider的Services”),他们需要继承ProviderBase。.NET框架包含了在服务和数据源之间定义协议的ProviderBase派生类。例如,MembershipProvder类继承于ProviderBase,它定义了成员资格服务和成员资格数据源之间的接口。想要创建自定义成员资格Provider的开发者应该继承于MembershipProvider类而不是ProviderBase。图3展示了provider类的层次。

图3 ASP.NET 2.0 provider类

Provider注册和配置

Providers在它们提供服务的特征和服务的配置节点里的<providers>配置节点中被注册。例如,成员资格providers以这种方式被注册:

<configuration>

<system.web>

<membership ...>

<providers>

<!-- Membership providers registered here -->

</providers>

</membership>

...

</system.web>

</configuration>

而角色providers以这种方式被注册:

<configuration>

<system.web>

<roleManager ...>

<providers>

<!-- Role providers registered here -->

</providers>

</roleManager>

...

</system.web>

</configuration>

在<providers>节点中的<add>节点注册providers并让它们可用。<add>节点提供了一组基本的配置属性,例如名称,类型,描述,加上每一个provider都不一样的的特定provider配置属性:

<configuration>

<system.web>

<membership ...>

<providers>

<add name="AspNetSqlMembershipProvider"

type="[Type name]"

description="SQL Server membership provider"

connectionStringName="LocalSqlServer"

...

/>

...

</providers>

</membership>

...

</system.web>

</configuration>

一旦被注册后,provider通常使用在相应配置节点中的defaultProvider属性来标记成默认活动provider。例如,以下的<membership>节点标记SqlMembershipProvider作为成员资格服务默认活动的provider。

<membership defaultProvider="AspNetSqlMembershipProvider">

<providers>

...

</providers>

</membership>

defaultProvider属性通过逻辑名称而不是类型名称来识别注册在<providers>中的provider。注意ASP.NET在使用defaultProvider属性上并不是完全一致的。例如,<sessionState>节点使用customProvider属性来指定默认会话状态provider。

为一特定服务的注册的providers可能有多个,但是只有一个是默认的。为一特定服务注册的所有providers通过服务的Provider属性(例如,Membership.Providers)在运行时中被列举出来。

构建自定义Providers需要注意的事项

所有的providers通常有特定特征。所有的,例如,当ASP.NET运行时调用继承自ProviderBase的Initialize方法时初始化他们自己,所有的都必须是线程安全的。以下部分描述了应用于所有providers的关键原则和模式,无论类型。

Provider初始化

所有的provider类都继承自,直接或间接的,System.Configuration.Provider.ProviderBase。同样地,他们继承了一个叫做Initialize的虚方法,这个方法在provider装载的时候被ASP.NET调用。继承的方法需要重写Initialize并执行对每个provider特定的初始化。一个重写的Initialize方法需要执行以下任务:

1. 确保provider有权限。如果没有它需要抛出一个异常。(作为选择,provider可以使用声明的变量,例如System.Security.Permisions.FileIOPermissionAttribute,来确保它有必须的权限。)

2. 确保传给Initialize的config参数非空并在空时抛出一个ArgumentNullException异常。

3. 调用基类的Initialize,确保传给基类的name参数既不为空也不为空的字符串(如果它为空指派一个默认的名字),如果config当前缺少一个description属性或者该属性为空时,向传递给基类的config参数中添加一个默认的description属性。

4. 通过读取和应用嵌入在config中的配置属性来配置它自己,确保调用Remove在每个被识别的配置属性。配置属性可以使用NameValueCollection的字符串索引器来复去到。

5. 如果config.Count>0,抛出一个ProviderException异常,意味着用来注册provider的节点包含着一个或多个未被识别的配置属性。

6. 将provider为运行事例需要的其他事情完成,从XML文件中读取状态因为这个文件每次请求的时候不需要被重新加载。尽管如此,需要特别注意的是,Initialize不要调用任何provider提供的服务的特征APIs,因为如果这么做可能会造成无穷的迭代。例如,通过成员资格provider的Initialize方法创建一个MembershipUser对象会使Initialize被再次调用。

以下的代码展示 了一个对SQL Server provider的规范Initialize方法,这个provider识别一个名字为connectionStringName的provider特定配置属性。这是一个对你以后规范代码的非常好的例子,它与内置的SQL Server providers,例如SqlMembershipProvider,其中的Initialize方法非常接近了。

public override void Initialize(string name,

NameValueCollection config)

{

// Verify that the provider has sufficient trust to operate. In

// this example, a SecurityException will be thrown if the provider

// lacks permission to call out to SQL Server. The built-in

// providers tend to be less stringent here, simply ensuring that

// they're running with at least low trust.

SqlClientPermission.Demand ();

// Verify that config isn't null

if (config == null)

throw new ArgumentNullException ("config");

// Assign "name" a default value if it currently has no value

// or is an empty string

if (String.IsNullOrEmpty (name))

name = "SampleSqlProvider";

// Add a default "description" attribute to config if the

// attribute doesn't exist or is empty

if (string.IsNullOrEmpty (config["description"])) {

config.Remove ("description");

config.Add ("description", "Sample SQL provider");

}

// Call the base class's Initialize method

base.Initialize(name, config);

// Initialize _connectionStringName from the connectionStringName

// configuration attribute, or throw an exception if the attribute

// doesn't exist or is an empty string, or if it designates a

// nonexistent connection string

string connect = config["connectionStringName"];

if (String.IsNullOrEmpty (connect))

throw new ProviderException

("Empty or missing connectionStringName");

config.Remove ("connectionStringName");

if (WebConfigurationManager.ConnectionStrings[connect] == null)

throw new ProviderException ("Missing connection string");

_connectionString = WebConfigurationManager.ConnectionStrings

[connect].ConnectionString;

if (String.IsNullOrEmpty (_connectionString))

throw new ProviderException ("Empty connection string");

// Throw an exception if unrecognized attributes remain

if (config.Count > 0) {

string attr = config.GetKey (0);

if (!String.IsNullOrEmpty (attr))

throw new ProviderException ("Unrecognized attribute: " +

attr);

}

}

在以上的代码中,connectionStringName是一个必须的配置属性。因此,如果该属性不存在Initialize会抛出一个异常。一些属性是可选的而非必须的。如果是一个可选属性,如果它不存在,Initialize会给相应的字段或参数指派一个默认的值。

Provider 生命周期

当应用程序使用providers第一次访问一个相应服务的feature时,它们会被加载。它们在每个应用程序中(也就是说,在应用程序领域)只被实例化一次。Provider的生命周期大体上与应用程序的相当,因此可以安全的创建在字段中存储状态的“状态化的”providers。这种“每个应用程序一个实例”的模型对请求间的连续数据是非常方便的。但是它有一个缺点,这个缺点在下部分中会被讲到。

线程安全

通常,ASP.NET 竭尽全力组织开发者书写线程安全的代码。例如HTTP 处理程序和HTTP指令集,根据每个请求(每一个线程)来实例化,于是除非访问共享状态,否则它们就不是线程安全的。

Providers是“一次线程一次初始化一次”规则的例外。ASP.NET 2.0 providers在应用程序的生命周期内的任意时间都可被实例化,并且被所有的请求共享。因为每一个请求是被向ASP.NET提供服务的线程池中的不同线程处理,providers能被(或者将要被)两个或者更多的线程在同一时间访问。这就意味着providers必须是线程安全的。包含非线程安全代码的Providers开始可能能正常使用(在小负载的情况下也可能使用),但是有可能在负载增大时会产生错误的、再生数据困难等错误,并发执行的请求线程增多。

非线程安全的唯一provider方法是继承自ProviderBase的Initialize方法。ASP.NET确保Initialize不会被两个或更多的线程同时调用执行。

关于线程安全代码的详细讨论超过了本文的范畴,但是大多数并发执行线程错误的起因是当两个或者更多的线程同时访问同一个数据且至少有一个线程在执行写操作而非所有的都是在读取数据。请考虑,例如,以下类定义:

public class Foo

{

private int _count;

public int Count

{

get { return _count; }

set { _count = value; }

}

}

假设两个线程每一个都引用了一个Foo实例(也就是说,有两个线程,但是只有一个Foo对象),一个线程在读取该对象的Count属性,同时另一个线程在执行写操作。如果读和写正好发生在同一时间,执行读取操作的线程可能可能得到了一个假的值。一个解决方案是使用框架类库中的System.Threading.Monitor 类(或者类似的,使用C# lock关键字)来连续访问_count,这是线程安全的Foo的一个标准版本:

public class Foo

{

private int _count;

private object _syncLock = new object ();

public int Count

{

get { lock (_syncLock) { return _count; } }

set { lock (_syncLock) { _count = value; } }

}

}

System.Threading命名空间也包含了其他的同步类,包括一个ReaderWriterLock类,该类可允许任意数目的线程来同时读取共享数据且阻止重复读,也可同时执行写操作并阻止重复写。

(与之形成对比的是,Monitor类是限制在同一时间对同一线程访问的,甚至是所有的线程都是执行读取操作。)此外,System.Runtime.CompilerServices.MethodImplAtttibute类能被用来连续访问完整的方法:

  [MethodImpl (MethodImplOptions.Synchronized)]

public void SomeMethod ()

{

// Only one thread at a time may execute this method

}

尽管如此,MethodImpl同步逊于Monitor同步,ReaderWriterLock和其他的System.Threading类,因为它在锁线程方面的不足。在最好的情况下,MethodImpl(MethodImplOptions.Synchronized)在对象层进行锁,类似于锁定自己。在特定的环境下,它是在类型层甚至在应用程序领域层进行锁,它们中的任意一个都会降低性能和可测量性。

这儿是一些关于线程安全的关键点,需要在书写和测试自定义providers时紧记。

· 在Initialize方法之外,一个线程安全的provider能连续的读/写所有的实例数据,包括字段。如果访问是只读的,它并不需要被连续化。例如,一个provider的Initialize方法可能从web.config方法中读取连接字符串,并将它存放到(写到)一个实例的字段中。写操作并不需要同步因为它在Initialize中被执行,而Initialize是不能被并发的线程同时调用的。如果在Initialize之外对这个字段的所有访问是读操作而非写,那么这些访问也都不需要同步。

· 一个线程安全的provider不需要连续访问本地变量或者其他基于堆栈的数据。

· 永远不要向lock或者Monitor.Enter中传递一个值类型(例如int或者struct).编译器不会让你这么做,因为如果这种事情发生了,由于值类型将被装箱,你将得不到任何同步。这就是为什么线程安全的Foo对_syncLock实用锁而非_count,_syncLock是一个引用类型,而_count不是。

· 测试线程安全的自定义provider的最好方式是让它在有多个处理器的机器上大负荷使用。

关于更多的在代码中同步并发执行线程,请参考以下资源:

· “Safe Thread Synchronization”,Jeffrey Richter.文章在MSDN 2003.1

· “Programming Microsoft .NET”的 第14章,作者为Jeff Prosise(2002,Microsoft出版)

本地化

为了简单,以上的provider代码(以及在本文其他地方的)硬编码了错误信息、provider描述信息以及其他的文本字符串。打算被更大规模使用的provider应该避免硬编码字符串而是应该使用本地化字符串资源来代替。

原子性

一些provider操作包含对数据源的多重更新。例如,一个角色管理provider的AddUsersToRoles方法能够在一次操作中添加多个用户到多个角色中,因此可能要求对数据源的多次更新操作。当数据源支持事务处理(例如,当数据源是SQL Server),推荐使用事务来确保更新的原子性,也就是说,如果在连续的更新操作中,如果有一个失败,需要将已经完成的更新进行回滚。如果数据源不支持事务处理,provider的开发者要确保更新操作要麽全部完成要麽全都不执行。

异常及异常类型

.NET 框架类库的System.Configuration.Provider命名空间包含着叫做ProviderException的多用途的异常类,它使providers能够在错误发生时抛出异常。通常,异常类型应该尽可能的详细准确。例如,当一个空引用传给了要求非空引用的方法时,provider应该抛出一个ArgumentNullException异常。类似的,当一个空字符串传给了一个要求非空字符串的方法时,provider应该抛出ArgumentException异常。

更多的基本错误,例如让角色管理provider删除一个不存在的角色,应该抛出一个ProviderExceptions异常来处理。如果需要,你可以定义你自己的异常错误类,并在使用ProviderExceptions的场所使用它们。包含在ASP.NET 2.0中的providers使用ProviderException来标记错误。

执行完全

  一个provider并不被要求实现所有它继承的基类定义的所有规则。例如,成员资格管理Provider可能选择不实现继承自MembershipProvider的GetNumberOfUserOnline方法.(方法必须在基类中被重写,以满足编译器的要求,但是它的实现可能就是简单的抛出一个NotSupportedException异常)。显然,实现的越完整越好,开发者应该注意记录不被支持的特征功能。