ASP.NET 2.0 Provider Model: Introduction to the Provider Model
Summary: This series of articles is a drill down in the Microsoft-built ASP.NET provider objects, in Visual Basic code. (120 printed pages)
Review the Visual C# version.
Introduction to the Provider Model (14 printed pages)
Membership Providers (24 printed pages)
Role Providers (13 printed pages)
Site Map Providers (20 printed pages)
Session State Providers (21 printed pages)
Profile Providers (23 printed pages)
Web Event Providers (14 printed pages)
Web Parts Personalization Providers (12 printed pages)
Custom Provider-Based Services (10 printed pages)
Hands-on Custom Providers: The Contoso Times (3 printed pages)
ASP.NET 2.0 includes a number of services that store state in databases and other storage media. For example, the session state service manages per-user session state by storing it in-process (in memory in the application domain of the host application), in memory in an external process (the "state server process"), or in a Microsoft SQL Server database, while the membership service stores user names, passwords, and other data in Microsoft SQL Server databases or Active Directory. For the majority of applications, the built-in storage options are sufficient. However, the need sometimes arises to store state in other media such as Oracle databases, DB2 databases, Microsoft SQL Server databases with custom schemas, XML files, or even data sources front-ended by Web services.
In ASP.NET 1.x, developers who wished to store state in alternative storage media were faced with the daunting prospect of rewriting large portions of ASP.NET. ASP.NET 2.0, by contrast, introduces extreme flexibility to state storage. ASP.NET 2.0 services that manage state do not interact directly with storage media; instead, they use providers as intermediaries, as pictured in Figure 1.
Figure 1. The ASP.NET 2.0 provider model
A provider is a software module that provides a uniform interface between a service and a data source. Providers abstract physical storage media, in much the same way that device drivers abstract physical hardware devices. Because virtually all ASP.NET 2.0 state-management services are provider-based, storing session state or membership state in an Oracle database rather than a Microsoft SQL Server database is as simple as plugging in Oracle session state and membership providers. Code outside the provider layer needn't be modified, and a simple configuration change, accomplished declaratively through Web.config, connects the relevant services to the Oracle providers.
Thanks to the provider model, ASP.NET 2.0 can be configured to store state virtually anywhere. Membership data, for example, could just as easily come from a Web service as from a database. All that's required is a custom provider. Some companies will prefer to acquire custom providers from third parties. Others, however, will want to write their own, either because no suitable provider is available off the shelf, or because they wish to adapt ASP.NET to legacy storage media (for example, existing membership databases). This whitepaper documents the ASP.NET 2.0 provider model and provides the critical information that developers need to write robust, high-quality providers.
The ASP.NET 2.0 provider model was designed with the following goals in mind:
- To make ASP.NET state storage both flexible and extensible
- To insulate application-level code and code in the ASP.NET run-time from the physical storage media where state is stored, and to isolate the changes required to use alternative media types to a single well-defined layer with minimal surface area
- To make writing custom providers as simple as possible by providing a robust and well-documented set of base classes from which developers can derive provider classes of their own
It is expected that developers who wish to pair ASP.NET 2.0 with data sources for which off-the-shelf providers are not available can, with a reasonable amount of effort, write custom providers to do the job.
Figure 2 depicts the provider model as it applies to the ASP.NET membership service. In the top layer are the login controls: Login, LoginView, and others that provide UIs for logging in users, recovering lost passwords, and more. Underneath the login controls sits the membership service, which provides the public API used by the controls and that can be used by application code as well. The membership service stores login credentials and other information in a membership data source, but rather than access the data source directly, it interacts with it through a membership provider. Thus, the login controls and the membership service itself can be adapted to different types of data sources (for example, Oracle databases) simply by adding new providers.
Figure 2. The membership provider model
Membership providers implement a well-defined interface consisting of methods and properties defined in an abstract base class named MembershipProvider. Because all membership providers are built to the same contract, the membership service can interact with them without knowing or caring how they choose to store the data.
Membership is one of several ASP.NET 2.0 services that use the provider architecture. The following table documents the features and services that are provider-based and the default providers that service them:
|Feature or Service||Default Provider|
|Web events||N/A (see below)|
|Web Parts personalization||System.Web.UI.WebControls.WebParts.SqlPersonalizationProvider|
|Protected configuration||N/A (see below)|
SQL providers such as SqlMembershipProvider and SqlProfileProvider use Microsoft SQL Server or SQL Server Express as their data source. InProcSessionStateStore stores session state in memory, while XmlSiteMapProvider uses XML files as its data source. N/A (Not Applicable) designates features and services for which providers must be explicitly identified. These include:
- Web events, which must be explicitly mapped to providers in the <healthMonitoring> configuration section. The master Web.config file maps certain Web events to System.Web.Management.EventLogWebEventProvider, causing them to be logged in the Windows event log without any setup.
- Protected configuration, which requires callers who invoke its encryption services to specify a provider. The Aspnet_regiis.exe tool that comes with ASP.NET uses protected configuration to encrypt and decrypt configuration sections. Unless instructed to do otherwise, Aspnet_regiis.exe calls upon System.Configuration.RsaProtectedConfigurationProvider to provide encryption and decryption services.
ASP.NET 2.0 ships with the following providers:
|Provider Type||Built-In Provider(s)|
|Web Parts personalization||System.Web.UI.WebControls.WebParts.SqlPersonalizationProvider|
In addition, Microsoft intends to make a set of providers targeting Microsoft Access available as a free download. Developers are discouraged from using Microsoft Access on the back end of enterprise applications, but Microsoft realizes that Access may be appropriate for small Web sites that serve a very limited number of users.
The System.Configuration.Provider namespace includes a class named ProviderBase that serves as the root class for all providers. ProviderBase is protoyped as follows:
Public MustInherit Class ProviderBase Public Overridable ReadOnly Property Name As String Public Overridable ReadOnly Property Description As String Public Overridable Sub Initialize (name As String, _ config As NameValueCollection) End Class
The Name property returns the provider's name (for example, "AspNetSqlMembershipProvider"), while Description returns a textual description. Initialize is called by ASP.NET when the provider is loaded, affording the provider the opportunity to initialize itself. The name parameter contains the provider's name; its value comes from the name attribute of the <add> element that registered the provider, as in
<add name="AspNetSqlMembershipProvider" ... />
The config parameter contains the remaining name/value pairs present in the <add> element.
The default implementation of Initialize ensures that Initialize hasn't been called before and initializes Name and Description from the configuration attributes of the same name. ProviderBase's source code appears as follows.
Imports System.Collections.Specialized Imports System.Runtime.Serialization Namespace System.Configuration.Provider Public MustInherit Class ProviderBase Private _name As String Private _description As String Private _initialized As Boolean Public Overridable ReadOnly Property Name() As String Get Return _name End Get End Property Public Overridable ReadOnly Property Description() As String Get Return CStr(IIf(String.IsNullOrEmpty(_description), _ _name, _description)) End Get End Property Public Overridable Sub Initialize(ByVal name As String, _ ByVal config As NameValueCollection) SyncLock Me If _initialized Then Throw New InvalidOperationException("...") End If _initialized = True End SyncLock If (name = Nothing) Then Throw New ArgumentNullException("name") End If If (name.Length = 0) Then Throw New ArgumentException("...", "name") End If _name = name If config IsNot Nothing Then _description = config("description") config.Remove("description") End If End Sub End Class End Namespace
Developers typically derive from ProviderBase only if they're writing custom services that are provider-based (see Custom Provider-Based Services). The .NET Framework includes ProviderBase derivatives that define contracts between services and data sources. For example, the MembershipProvider class derives from ProviderBase and defines the interface between the membership service and membership data sources. Developers writing custom membership providers should derive from MembershipProvider rather than ProviderBase. Figure 3 shows the provider class hierarchy.
Figure 3. ASP.NET 2.0 provider classes
Providers are registered in <providers> configuration sections in the configuration sections of the features and services that they serve. For example, membership providers are registered this way:
<configuration> <system.web> <membership ...> <providers> <!-- Membership providers registered here --> </providers> </membership> ... </system.web> </configuration>
While role providers are registered like this:
<configuration> <system.web> <roleManager ...> <providers> <!-- Role providers registered here --> </providers> </roleManager> ... </system.web> </configuration>
<add> elements within <providers> elements register providers and make them available for use. <add> elements support a common set of configuration attributes such as name, type, and description, plus provider-specific configuration attributes that are unique to each provider:
<configuration> <system.web> <membership ...> <providers> <add name="AspNetSqlMembershipProvider" type="[Type name]" description="SQL Server membership provider" connectionStringName="LocalSqlServer" ... /> ... </providers> </membership> ... </system.web> </configuration>
Once registered, a provider is usually designated as the default (active) provider using the defaultProvider attribute of the corresponding configuration element. For example, the following <membership> element designates SqlMembershipProvider as the default provider for the membership service:
<membership defaultProvider="AspNetSqlMembershipProvider"> <providers> ... </providers> </membership>
The defaultProvider attribute identifies by logical name (rather than type name) a provider registered in <providers>. Note that ASP.NET is not entirely consistent in its use of the defaultProvider attribute. For example, the <sessionState> element uses a customProvider attribute to designate the default session state provider.
Any number of providers may be registered for a given service, but only one can be the default. All providers registered for a given service may be enumerated at run-time using the service's Providers property (for example, Membership.Providers).
All providers have certain characteristics in common. All, for example, initialize themselves when the ASP.NET run-time calls the Initialize method that they inherit from ProviderBase, and all must be thread-safe. The sections that follow document the key principles and patterns that apply to all providers, regardless of type.
All provider classes derive, either directly or indirectly, from System.Configuration.Provider.ProviderBase. As such, they inherit a virtual method named Initialize that's called by ASP.NET when the provider is loaded. Derived classes should override Initialize and use it to perform provider-specific initializations. An overridden Initialize method should perform the following tasks:
- Make sure the provider has the permissions it needs to run and throw an exception if it doesn't. (Alternatively, a provider may use declarative attributes such as System.Security.Permissions.FileIOPermissionAttribute to ensure that it has the necessary permissions.)
- Verify that the config parameter passed to Initialize isn't null and throw an ArgumentNullException if it is.
- Call the base class version of Initialize, ensuring that the name parameter passed to the base class is neither null nor an empty string (and assigning the provider a default name if it is), and adding a default description attribute to the config parameter passed to the base class if config currently lacks a description attribute or the attribute is empty.
- Configure itself by reading and applying the configuration attributes encapsulated in config, making sure to call Remove on each recognized configuration attribute. Configuration attributes can be read using NameValueCollection's string indexer.
- Throw a ProviderException if config.Count > 0, which means that the element used to register the provider contains one or more unrecognized configuration attributes.
- Do anything else the provider needs to do to get itself ready to run-for example, read state from an XML file so the file needn't be reparsed on each request. However, it's critical that Initialize not call any feature APIs in the service that the provider serves, because doing so may cause infinite recursion. For example, creating a MembershipUser object in a membership provider's Initialize method causes Initialize to be called again.
The code below shows a canonical Initialize method for a SQL Server provider that recognizes one provider-specific configuration attribute named connectionStringName. This is a great example to pattern your code after and it closely parallels the Initialize methods found in built-in SQL Server providers such as SqlMembershipProvider.
Public Overrides Sub Initialize(ByVal name As String, _ ByVal config As NameValueCollection) ' 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 = Nothing) Then Throw New ArgumentNullException("config") End If If String.IsNullOrEmpty(name) Then name = "SampleSqlProvider" End If ' Add a default "description" attribute to config if the ' attribute doesn't exist or is empty If String.IsNullOrEmpty(config("description")) Then config.Remove("description") config.Add("description", "Sample SQL provider") End If ' Call the base class's Initialize method MyBase.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 Dim connect As String = config("connectionStringName") If String.IsNullOrEmpty(connect) Then Throw New ProviderException("Empty or missing connectionStringName") End If config.Remove("connectionStringName") If (WebConfigurationManager.ConnectionStrings(connect) = Nothing) Then Throw New ProviderException("Missing connection string") End If _connectionString = _ WebConfigurationManager.ConnectionStrings(connect).ConnectionString If String.IsNullOrEmpty(_connectionString) Then Throw New ProviderException("Empty connection string") End If If (config.Count > 0) Then Dim attr As String = config.GetKey(0) If Not String.IsNullOrEmpty(attr) Then Throw New ProviderException(("Unrecognized attribute: " + attr)) End If End If End Sub
In the above code, connectionStringName represents a required configuration attribute. Consequently, Initialize throws an exception if the attribute isn't present. Some attributes may be optional rather than required. In the case of an optional attribute, Initialize should assign a sensible default value to the corresponding field or property if the attribute isn't present.
Providers are loaded when the application using them first accesses a feature of the corresponding service, and they're instanced just once per application (that is, per application domain). The lifetime of a provider roughly equals the lifetime of the application, so one can safely write "stateful" providers that store state in fields. This one-instance-per-application model is convenient for persisting data across requests, but it has a downside. That downside is described in the next section.
In general, ASP.NET goes to great lengths to prevent developers from having to write thread-safe code. HTTP handlers and HTTP modules, for example, are instanced on a per-request (per-thread) basis, so they don't have to be thread-safe unless they access shared state.
Providers are an exception to the one-instance-per-thread rule. ASP.NET 2.0 providers are instanced one time during an application's lifetime and are shared among all requests. Because each request is processed on a different thread drawn from a thread pool that serves ASP.NET, providers can (and probably will be) accessed by two or more threads at the same time. This means providers must be thread-safe. Providers containing non-thread-safe code may seem to work at first (and may work just fine under light loads), but are likely to suffer spurious, hard-to-reproduce data-corruption errors when the load-and consequently, the number of concurrently executing request-processing threads-increases.
The only provider method that doesn't have to be thread-safe is the Initialize method inherited from ProviderBase. ASP.NET ensures that Initialize is never executed on two or more threads concurrently.
A comprehensive discussion of thread-safe code is beyond the scope of this document, but most threading-related concurrency errors result when two or more threads access the same data at the same time and at least one of those threads is writing rather than reading. Consider, for example, the following class definition:
Public Class Foo Private _count As Integer Public Property Count As Integer Get Return _count End Get Set _count = value End Set End Property End Class
Suppose two threads each hold a reference to an instance of Foo (that is, there are two threads but only one Foo object), and one thread reads the object's Count property at the same time that the other thread writes it. If the read and write occur at precisely the same time, the thread that does the reading might receive a bogus value. One solution is to use the Framework's System.Threading.Monitor class (or its equivalent, Visual Basic .NET's SyncLock keyword) to serialize access to _count. Here's a revised version of Foo that is thread-safe:
Public Class Foo Private _count As Integer Private _syncLock As Object = New Object Public Property Count() As Integer Get SyncLock _syncLock Return _count End SyncLock End Get Set(ByVal value As Integer) SyncLock _syncLock _count = value End SyncLock End Set End Property End Class
The System.Threading namespace includes other synchronization classes as well, including a ReaderWriterLock class that allows any number of threads to read shared data concurrently but prevents overlapping reads and writes as well as overlapping writes. (By contrast, the Monitor class restricts access to one thread at a time, even if all threads are readers.) In addition, the System.Runtime.CompilerServices.MethodImplAttribute class can be used to serialize access to entire methods:
<MethodImpl(MethodImplOptions.Synchronized)> _ Public Sub SomeMethod() ' Only one thread at a time may execute this method End Sub
However, synchronizing with MethodImpl is inferior to synchronizing with Monitor, ReaderWriterLock, and other System.Threading classes due to its coarseness in locking out threads. At best, MethodImpl (MethodImplOptions.Synchronized) locks at the object level-the equivalent of SyncLock (Me). Under certain circumstances, it locks at the type level or even at the application domain level, either of which adversely affects performance and scalability.
Here are some key points regarding thread safety to keep in mind as you write and test custom providers:
- Outside of the Initialize method, a thread-safe provider serializes read/write accesses to all instance data, including fields. Accesses need not be serialized if they are read-only. For example, a provider's Initialize method might read a connection string from Web.config and store it in (write it to) an instance field. The write doesn't have to be synchronized because it's performed in Initialize, which never executes on concurrent threads. If all accesses to the field outside of Initialize are reads rather than writes, then those accesses do not require synchronization, either
- A thread-safe provider does not have to serialize accesses to local variables or other stack-based data
- Never pass a value type (such as an int or a struct) to SyncLock or Monitor.Enter. The compiler won't let you because if it did, you'd get no synchronization at all due to the fact that the value type would be boxed. That's why the thread-safe version of Foo uses SyncLock _syncLock rather than SyncLock _count. _syncLock is a reference type; _count is not
- The best way to test the thread safety of a custom provider is to subject it to heavy loads on a multiprocessor machine
For more information on synchronizing concurrently executing threads in managed code, refer to the following resources:
- Safe Thread Synchronization by Jeffrey Richter. Article in the January 2003 issue of MSDN Magazine.
- Chapter 14 of the book "Programming Microsoft .NET" by Jeff Prosise (2002, Microsoft Press).
For simplicity, the provider code above (and elsewhere in this document) hard-codes error messages, provider descriptions, and other text strings. Providers intended for general consumption should avoid hard-coded strings and use localized string resources instead.
Some provider operations involve multiple updates to the data source. For example, a role provider's AddUsersToRoles method is capable of adding multiple users to multiple roles in a single operation and therefore may require multiple updates to the data source. When the data source supports transactions (for example, when the data source is SQL Server), it is recommended that you use transactions to ensure the atomicity of updates-that is, to roll back already-completed updates if a subsequent update fails. If the data source doesn't support transactions, the provider author is responsible for ensuring that updates are performed either in whole or not at all.
Exceptions and Exception Types
The .NET Framework's System.Configuration.Provider namespace includes a general-purpose exception class named ProviderException that providers can throw when errors occur. In general, exception types should be as specific as possible. For example, when a null reference is passed to a method that requires a non-null reference, the provider should throw an ArgumentNullException. Similarly, when an empty string is passed to a method that requires a non-empty string, the provider should throw an ArgumentException.
More general errors, such as asking a role provider to delete a role that doesn't exist, can be handled by throwing ProviderExceptions. If desired, you can define your own error-specific exception classes and use them in lieu of ProviderExceptions. The providers included with ASP.NET 2.0 use ProviderExceptions extensively to flag errors.
A provider is not required to implement the full contract defined by the base class it derives from. For example, a membership provider may choose not to implement the GetNumberOfUsersOnline method inherited from MembershipProvider. (The method must be overridden in the derived class to satisfy the compiler, but its implementation might simply throw a NotSupportedException.) Obviously, the more complete the implementation the better, and developers should take care to document any features that aren't supported.
Click here to continue on to part 1, Membership Providers.