Provider Model Design Pattern and Specification, Part 1

 

Rob Howard
Microsoft Corporation

March 2, 2004

Summary: Rob Howard returns to Nothin' But ASP.NET to look at how you can use the new "Whidbey" Provider design pattern to make extending your ASP.NET 1.1 applications easier and make them more flexible. (15 printed pages)

Related Books

This column has been quiet for some time—sorry about that. I have a really, really good excuse, though: We've been busy working on the next version of the Microsoft® .NET Framework and Microsoft® Visual Studio® .NET, code-named "Whidbey."

Busy is actually an understatement. For Microsoft® ASP.NET, we've roughly doubled the number of class libraries! Is that a good thing? Yes it is, and in the coming months we'll explore some of these new ASP.NET features. However, before focusing all of our attention towards Whidbey, we'll spend the first several articles on ASP.NET 1.1 and relate it to what's to come in Whidbey.

Our first article is about the new Whidbey provider-model design pattern and how you can start using it in your ASP.NET 1.1 applications today.

There has been a lot of buzz recently surrounding the announcement of the next version of Microsoft® Visual Studio® .NET and the Microsoft® .NET Framework code-named "Whidbey." No, this isn't another Whidbey article, but it is an article that discusses an important new design pattern that is going to show up in Whidbey, and more importantly, a pattern you can start using in your applications today.

In the next article, we'll look at implementing this pattern in an ASP.NET 1.1 application. In this article, we'll discuss why the pattern is needed, and walk through the "Whidbey" provider specification. Let's look at some of the reasons why we need the provider design pattern first.

Fixed Choices

In published APIs you are usually limited to a fixed number of options. An example I commonly use is the Microsoft® ASP.NET Session State feature. ASP.NET Session State supports an "out-of-process" mode. Using either Microsoft® SQL Server or the ASP.NET State Server, you can store user's Session data in a process separate from the running application. There are many benefits: reliability, easy Web farm support, and scalability. Most people tend to lean towards the database solution, since it allows for replication and other benefits not offered by the State Server. One small problem: What if you're not a SQL Server customer?

Fixed Internal Behaviors

Here's another common situation, again using Session State. Say you want to change some internal behaviors of ASP.NET APIs, for example changing how ASP.NET Session sets or retrieves values from its out-of-process store. Today you're constrained by the design. It is all or nothing: either we fetch all your data, or we fetch none of your data. If you're storing 5 MB of data and only need 50 KB to satisfy the current request... You get the picture.

Fixed Data Schema

The final scenario relevant to this discussion concerns schema. A schema is the logical model of how data is stored. For example, if you ever used Microsoft® Site Server 3.0 Personalization, it forced you to move your data into the schema that it desired. That's all well and good, but typically the people who built that schema don't know a whole lot about your business. In other words, fixed schemas are typically built to be really good for everyone, but excellent for no one.

These are common problems that all technologies share: the inability to change core behavior or functionality of published APIs.

Who Cares, I'll Write My Own

You definitely can create your own solutions for all these problems, but there are a lot of benefits to using applications with published APIs. One of the main benefits is that many developers know and understand what these APIs are and how they work. If you need to augment your staff, for instance, you can hire a consultant that is already familiar with these well-documented APIs. If you write your own, guess what; you also get to pay that person to learn your proprietary system.

What we really want is a way for everyone to get the benefits of well-documented and familiar programming interfaces, such as Session State, but the ability to completely control the internal Business Logic Layer (BLL) and Data Access Layer (DAL) of these APIs.

Introducing the Provider Design Pattern

The Provider Design Pattern is a pattern we've used in several of our ASP.NET Starter Kits, formalized in ASP.NET Whidbey, and one that we'll hope you'll start using for your applications, too. The theory of the pattern is that it allows us to define a well-documented, easy-to-understand API, such as our new Whidbey Membership APIs, but at the same time give developers complete control over the internals of what occurs when those APIs are called.

The pattern was officially named in the summer of 2002 when we were designing the new Personalization feature of ASP.NET Whidbey. We were struggling to design the "right" schema that worked for everyone—the Personalization feature of ASP.NET Whidbey adds a new Profile class to which you can add strongly typed properties, for example, FirstName, whose values are stored in a database. The problem was that we needed a flexible design, but at the same time we wanted it to be easily extensible without everyone having to be a DBA; turns out this simple requirement is an incredibly challenging problem.

On the one hand, we could have designed the system to be entirely schema driven. This would have meant that we would have designed what I call, for lack of a better name, vertical tables in the database. Rather than using traditional columns, there would be a set of tables used to define the schema and a data table used to map data to a particular type. Each new item in the data table would correspond to a type in the schema table identifying what type of property it was. Below is a very simplistic data model of this design:

Figure 1. A simple model for personalization

Using this data model, we would see something like the data below in the Schema table:

Figure 2. Proposed schema table

And data in the PersonalizationData table similar to the data below:

Figure 3. PersonalizationData table

As you can see, this model maps the property type (such as FirstName) to a property value in the PersonalizationData table, such as FirstName: Rob.

While being infinitely flexible, this model starts breaking down with a large number of users and properties. For example, while testing this design, we used the data from the ASP.NET Forums. At the time, this was 200,000 users, each with about 15 properties. This resulted in 3,000,000 rows in the data table! An important benefit that SQL can provide, namely normalized data, is lost. We also knew that people would want to use standard SQL syntax for selecting from the tables for reporting and maintenance—for example, try running a report on this type of design for all who live in a certain zip code using the vertical table design—not easy!

The obvious solution was to use normal tables, but we didn't want users to have to call a DBA to modify the tables for incremental changes. For example, to add a new property called ZipCode would require adding a new column to the Person table.

The conundrum was that we could choose either a very flexible schema-based design that had scale limitations, or a super-scalable design that might be too inflexible for some people. Furthermore, beyond the simple data schema problems, we also wanted partitioned data access, and we wanted the Personalization APIs to be extensible. We wanted a lot!

What did we decide? Well, we decided that each customer scenario would be unique. We knew that we would eventually want to implement it ourselves on www.asp.net, and would want control over the data model. To solve the problem, we borrowed a pattern we had been experimenting with in the ASP.NET Forums.

The Pattern

The pattern itself is exceedingly simple and is given the name "provider" since it provides the functionality for an API. Defined, a provider is simply a contract between an API and the Business Logic/Data Abstraction Layer. The provider is the implementation of the API separate from the API itself. For example, the new Whidbey Membership feature has a static method called Membership.ValidateUser(). The Membership class itself contains no business logic; instead it simply forwards this call to the configured provider. It is the responsibility of the provider class to contain the implementation for that method, calling whatever Business Logic Layer (BLL) or Data Access Layer (DAL) is necessary.

There are some rules for how a provider behaves. A provider implementation must derive from an abstract base class, which is used to define a contract for a particular feature. For example, to create a membership provider for Oracle, you create a new class OracleMembershipProvider, which derives from MembershipProviderBase. The feature base class, for example, MembershipProviderBase, in turn derives from a common ProviderBase base class. The ProviderBase class is used to mark implementers as a provider and forces the implementation of a required method and property common to all providers. Figure 4 gives an example of the inheritance chain.

Click here for larger image.

Figure 4. A provider inheritance chain

The implementer must also define a configuration section entry used to load the provider. Below is an example of what this configuration entry looks like (borrowed from the ASP.NET Forums):

<configuration>
    <forums>
        <forums defaultProvider="SqlForumsProvider" 
          defaultLanguage="en-en" >
            <providers>
                <clear/>

                <add name = "SqlForumsProvider" 
                     type = "AspNetForums.Data.SqlDataProvider, 
                             AspNetForums.SqlDataProvider" 
                     connectionString = "[connection string]"
                     databaseOwner = "dbo"
                />

                <add name = "AccessForumsProvider" 
                     type = "AspNetForums.Data.AccessDataProvider, 
                             AspNetForums.AccessDataProvider" 
                     fileLocation = 
                     "~\data\AccessDataProvider\AspNetForums.mdb"
                />

            </providers>

        </forums>
    </forums>
</configuration>

We'll see what this all means momentarily.

The beauty of this pattern is you can create a new class, for example, OracleForumsProvider, by deriving from the ForumsProvider base class and use Oracle as your data store. Or, if you prefer to continue using SQL Server, but wish to change the behavior of a single method, you can derive from SqlForumsProvider, override the method whose behavior you want to change, and then add that new class to the <providers> section of configuration. More simply put: the provider design allows developers to publish well-known APIs and also allows for a rich, enterprise-level extensibility model.

The following information comes directly from the provider specification for ASP.NET Whidbey.

Common Behaviors/Characteristics

Below is a listing of characteristics common to providers.

Base class

A base class should have as few methods and properties as possible. This is desirable to encourage developers to write providers.

Threading

Providers should be free-threaded/thread safe, with one instance per application domain. Any provider-specific objects created more frequently (for example, one per request) should be created through provider APIs.

Factory methods

The abstract base class should support factory methods to create new objects wherever appropriate, for example, CreateUser().

If a feature allows a provider to create a framework object, but does not allow the provider to extend the object, the framework class should be sealed.

Complex objects created by a provider may keep track of the provider that created it, and expose it as a Provider property. This allows users of the feature to determine the provider that owns the data for the object. For example, when a new user is created with the Membership API, it may be useful for the developer to be aware of the provider that data for the object is stored in.

Administration

The object model for ProviderCollection should include APIs to add, remove, and clear the collection and to set parameters of individual providers in the collection.

Every feature that has providers should have a Provider property that returns type ProviderCollection, for example, Membershp.Providers.

Class naming and namespaces

The specific provider base classes should be named [Feature]ProviderBase, for example, MembershipProviderBase.

Implementations should be named [ImplementationType][Feature]Provider, for example, SqlRoleManagerProvider.

Server controls that allow selection of a provider should have a property named Provider. This property should default to the value of the defaultProvider attribute in the related configuration section for the feature. For example, the new <asp:login/> server control's default provider is equivalent to the Membership default provider. The list of providers available to the login control is restricted by the providers defined for use in the <membership> section.

Common naming patterns for provider classes

Figure 5 below calls out some of the common names and casing that should be used for various data stores (where name is [Name][Feature]Provider).

Figure 5. Suggested prefixes for provider class names

For data stores that are not named above, the name value should be Pascal cased.

Configuring the default provider

All features that have providers must have a configuration section and should define a defaultProvider attribute. If a default provider is not specified, the first item in the collection is considered the default. For example:

<configuration>
  <system.web>
    <roleManager defaultProvider="AspNetSqlProvider" ...>

      <providers>

        <add name="AspNetWindowsProvider" 
             type="System.Web.Security.WindowsTokenRoleProvider, 
                    System.Web, 
             Version=1.2.3300.0, Culture=neutral, 
             PublicKeyToken=b03f5f7f11d50a3a"
             description=" description here " />

        <add name="AspNetSqlProvider"
             type="System.Web.Security.SqlRoleProvider, System.Web, 
             Version=1.2.3300.0, Culture=neutral, 
             PublicKeyToken=b03f5f7f11d50a3a"
             description=" description here "
             connectionStringName="[name in <connectionStrings/>" />

      </providers>
    </roleManager>
  </system.web>
</configuration>

Specifying a defaultProvider attribute is not required, although it is highly recommended. All ASP.NET providers will specify a defaultProvider attribute.

Provider-friendly name or dictionary key

When defining a provider within configuration, it is required for the name attribute to be defined. Furthermore, it is recommended that provider names follow a pattern to easily distinguish who owns the providers. The suggested pattern, and the pattern followed by the ASP.NET team is: [Provider Creator][Data Store]Provider.

For example, the Membership provider capable of storing data in SQL Server is:

<add name="AspNetSqlProvider"
    type="System.Web.Security.SqlRoleProvider, System.Web, 
    Version=1.2.3300.0, Culture=neutral, 
    PublicKeyToken=b03f5f7f11d50a3a"
    description = "ASP.NET SQL Server provider"
    connectionStringName="[name in <connectionStrings/>" />

It is important to note the friendly name of the provider when specified in configuration is distinct from the class name of the provider. The friendly name does not need to include the feature name, for example, Role, since the friendly name values are only usable within the context of the feature, for example, Roles.Providers["AspNetSqlProvider"]. The provider creator detail is added to distinguish between providers that may access similar data stores but pose differing behavior.

Provider type

When defining a provider within configuration, it is required for the type attribute to be defined. The type value must be a fully qualified type name following the format:

       type="[namespace.class], [assembly name], 
             Version=[version], Culture=[culture], 
             PublicKeyToken=[public token]"

For example:

       type="System.Web.Security.SqlRoleProvider, System.Web, 
             Version=1.2.3300.0, Culture=neutral, 
             PublicKeyToken=b03f5f7f11d50a3a"

The strongly typed name is desired, however, it is also legitimate to use the shorter style assembly type name:

       type="System.Web.Security.SqlRoleProvider, System.Web"

Expected APIs

Features that derive from the various provider base classes should follow the common pattern of supporting a Provider property and Providers collection property that allow the developer to access the default configured provider as well as the other providers specified in configuration. For example:

SqlMembershipProvider p = (SqlMembershipProvider) Membership.Provider;

AccessMembershipProvider a = 
(AccessMembershipProvider)Membership.Providers["AspNetAccessProvider"];

The Provider property and Providers collection allow for runtime access to the underlying Provider class instance. The Provider property returns the currently configured default provider, while the Providers collection returns a collection of all the available providers defined within configuration.

Use of <connectionStrings/>

All providers that require a connection to a database or require the use of a connection string to locate their data store should use the <connectionStrings/> section of configuration. Additionally, the name of the property on the provider should be connectionStringName with the value being the named value in <connectionStrings/>.

A connection string value is required; there cannot be an internal default.

For all providers shipped by Microsoft, it should not be possible to store a connection string in the provider definition. Rather the <connectionStrings/> section should be used. This will allow us to encrypt the connection string, further securing the application.

An exception will be thrown when a provider attempts to use a connection string obtained from <connectionStrings/> that either does not exist or is invalid.

Description attribute

Although not an explicit requirement for all providers, any providers created by Microsoft should have a description attribute. The description attribute should contain a brief, friendly description that can be displayed in the Web administration tool or other displays.

The description attribute is optional and it is not an error for the description attribute to be missing; empty string is assumed.

String comparisons

When any string comparisons are done from with the configuration system for providers, a culture-invariant string comparison is performed. This rule applies to the name value defined for providers.

Conceptual Specification

The conceptual specification section addresses common classes shared by all providers, the collection class used to manage providers, and rules for how providers are managed within a features configuration section.

ProviderBase

All providers derive from a common ProviderBase class. By supporting this common abstract base class, all providers additionally share a common base type.

namespace System.Configuration.Provider {
  abstract class ProviderBase {

    // Methods
    public abstract void Initialize(string name, 
      NameValueCollection config);

    // Properties
    public abstract string Name { get; }
  }
}

When a new instance of a provider is created, internally the Initialize() method is called. It is assumed that all provider instances will add additional functionality to the provider beyond ProviderBase, for example, MembershipProvider. However, all providers should still derive from ProviderBase.

Figure 6 Initialize method for ProviderBase

Figure 7. Name property of ProviderBase

Figure 8. Add method of ProviderCollection

Figure 9. Clear method of ProviderCollection

ProviderCollection

The ProviderCollection is a utility collection class used to manage classes that derive from ProviderBase. Features that implement providers can use the ProviderCollection to add, remove, or clear all providers. Internally, instances are cast to the correct type when a typed provider is needed.

namespace System.Configuration.Provider {

  public class ProviderCollection : IEnumerable {

    // Methods
    //
    public void Add(ProviderBase provider);
    public void Clear();
    public void Remove(string name);
    public void SetReadOnly(string name);

    // Properties
    //
    public ProviderBase this(string name) { get; set; }
    public int Count { get; set; }
  }

}

Figure 10. Remove method of ProviderCollection

Figure 11. SetReadOnly property of ProviderCollection

Figure 12. Add method of ProviderCollection

For example, when Membership.Providers["AspNetSqlProvider"] is requested, internally the following code is assumed:

public MembershipProvider this [string name] {

  get { return (MembershipProvider) providerCollection[name]; }

  ...
}

Configuration

Features that make use of providers must implement a <providers> configuration section within their feature's configuration slot. It is within the <providers> section that providers may be added, inherited providers may be cleared, or individual providers may be removed.

Providers are defined at the machine or application level. Providers cannot be defined in web.config files in application directories that are not application roots.

Setting the default provider

All features that have providers should have a defaultProvider attribute on the main configuration section. If a default provider is not specified, the first item in the collection is considered the default. For example:

<configuration>
  <system.web>
    <roleManager defaultProvider="AspNetWindowsProvider" ...>

      <providers>

        <add name="Windows" 
             type="System.Web.Security.WindowsTokenRoleProvider, 
                    System.Web, 
             Version=1.2.3300.0, Culture=neutral, 
             PublicKeyToken=b03f5f7f11d50a3a"
             description=" description here " />

        <add name="AspNetSqlProvider"
             type="System.Web.Security.SqlRoleProvider, System.Web, 
             Version=1.2.3300.0, Culture=neutral, 
             PublicKeyToken=b03f5f7f11d50a3a"
             description=" description here "
             connectionStringName="[name in <connectionStrings/>" />

      </providers>
    </roleManager>
  </system.web>
</configuration>

Managing providers in configuration: <providers>

The <providers> configuration section contains 0 or more <add>, <remove>, or <clear> elements. The following rules apply when processing these elements:

  1. It is not an error to declare an empty <providers /> element.
  2. Providers inherit items from parent application configuration <add> statements.
  3. It is an error to redefine an item using <add> if the item already exists or is inherited.
  4. It is an error to <remove> a non-existing item.
  5. It is not an error to <add>, <remove>, and then <add> the same item again.
  6. It is not an error to <add>, <clear>, and then <add> the same item again.

<clear> removes all inherited items and items previously defined. For example, an <add> declared before a <clear> is removed, while an <add> declared after a <clear> is not removed.

Figure 13. Add element for configuration

New providers are added using the <add> element nested beneath <providers>.

Figure 14. Remove element for configuration

Figure 15. Clear element for configuration

Conclusion

The provider model can help you design flexible and easily evolved APIs for your applications. In fact, today it's being used in the ASP.NET Forums (http://forums.asp.net) as well as DotNetNuke (http://www.dotnetnuke.com), both of which are ASP.NET 1.1 applications. In Part 2 of this article, we'll examine an implementation of this specification/pattern that you can begin using in your ASP.NET 1.1 applications.

After we conclude with the next provider article, we'll look at how to build a scalable database cache invalidation system for ASP.NET 1.1—something that many have tried, but few have succeeded with (I even had a stab at this about 3 years ago). If you've got other articles you'd like to see, let me know. My contact information is below.

A First Look at ASP.NET v. 2.0

ASP.NET Developer's Cookbook

 

Nothin' But ASP.NET

Rob Howard is a program manager on the Microsoft Web Platform and Tools team, and helped design many of the new "Whidbey" infrastructure features (including the new provider design pattern). When he's not working, he plays hockey on the Dallas, TX Microsoft hockey team and spends time with his family. You can read more about what Rob's up to by visiting his blog at https://weblogs.asp.net/rhoward, or send him an e-mail message at rhoward@devadvice.com.