Beyond Membership: Using the Provider Design Pattern for Custom Features

 

Aaron Murrell
Software Architects

May 2006

Applies to:
   ASP.NET 2.0
   .NET Framework 2.0

Summary: ASP.NET 2.0 offers an abstract membership API for adding authentication and membership management functionality to your applications by leveraging a design pattern called the provider pattern. This article illustrates how to implement this pattern. (17 printed pages)

Contents

Introduction
The Provider Pattern in .NET 2.0
How It Works
Making Your Own Provider-Based Features
First Things First
Step 1: Design and Implement the Public Interface (API) for Your Feature
Step 2: Extend from ProviderBase
Step 3: Derive from ProviderCollection
Step 4: Implement a Provider
Conclusion

Introduction

ASP.NET 2.0 offers an abstract membership API for adding authentication and membership management functionality to your applications by leveraging a design pattern called the provider pattern. This is just one of a number of provider-based features available in version 2 of the .NET Framework. However, this provider design pattern can be employed far beyond features related to what comes in the box. In fact, you can use the same foundation classes that all of the .NET 2.0 provider-based features are built on, to create your own custom provider-based features.

The Provider Pattern in .NET 2.0

The provider pattern is what has been employed directly in version 2 of the .NET Framework to address the need for flexibility in how applications consume services from other applications. The concept is nothing new. In fact, if you have been using ADO for any length of time, you have likely been depending on this very pattern to conceal the complexities of interchangeably utilizing database implementations such as SQL Server and Oracle, whose native interfaces differ considerably. One of the most-recently well documented uses of this pattern in .NET 2.0 Whidbey is the Membership functionality inside the System.Web.Security namespace. The included Membership functionality allows you to use a variety of authentication data stores or services, including SQL Server, Active Directory, or your own custom data store, without changing the consuming application's code. As long as you code using the interface defined by the Membership API, simply swapping out the default Membership provider identified within your applications configuration file is all that is needed.

In .NET 2.0, we find that this approach is being used in several key functional areas. A quick glance at the inheritance hierarchy for the System.Configuration.Provider.ProviderBase class reveals the provider pattern hard at work in the areas of configuration, role management, session-state storage, and ASP.NET website navigation related tasks (see Table 1).

Table 1. Provider-based features in .NET 2.0

ClassDescription
System.Configuration.ProtectedConfigurationProviderProvides encryption and decryption services for protected configuration sections.
System.Configuration ProviderSettingsActs as a base class for deriving custom settings providers in the application settings architecture.
System.Web.Management.WebEventProviderDefines the base class for the creation of event providers.
System.Web.Security.MembershipProviderDefines the contract that ASP.NET implements to provide membership services using custom membership providers.
System.Web.Security.RoleProviderDefines the contract that ASP.NET implements to provide role management services using custom role providers.
System.Web.SessionState.SessionStateStoreProviderBaseDefines the required members of a session-state provider for a data store.
System.Web.SiteMapProviderProvides a common base class for all site map data providers, and a way for developers to implement custom site map data providers that can be used with the ASP.NET site map infrastructure as persistent stores for SiteMap objects.
System.Web.UI.WebControls.WebParts.PersonalizationProviderImplements the basic functionality for a personalization provider.

A set of core classes was created in .NET 2.0, to afford framework developers consistent and effective use of this pattern. The good news is that you can apply the benefits of this pattern to your own organization's needs, by leveraging the same set of core classes to create a flexible architecture for your application.

How It Works

Infrastructure code routes application messages from a generic interface to individual provider objects that are responsible for actually carrying out functional tasks using their own specific and unique implementation. The decision of which provider object actually receives the message from the interface at any given point in time is deferred from being written in code to the application's configuration file, granting you the flexibility to change the providers that your application uses for any given feature, simply by editing the file. The ability to do this in .NET 2.0 relies heavily on the application configuration system and the CLR type loader. Implementation of your own custom features requires implementing some plumbing to realize the instantiation of these objects, the configuration of provider behavior, and routing of messages to objects. Much of the work, however, has already been done for us, and exists in the .NET Framework, ready for us to use.

Making Your Own Provider-Based Features

I won't spend much time on the tenets of the provider pattern specification for .NET, because Rob Howard has an excellent MSDN article detailing its specifications. This article will instead focus on the implementation of those specifications, using the tools available to us. But suffice it to say, if you really want to get the most out of using the provider pattern for your function, you should be mindful of these tenets. You can also get an idea of how the provider pattern can be implemented in version 1.x of the .NET Framework by reading his follow-up article, which is part of the "Nothin' but ASP.NET" column.

Let's get started. Although there's a good bit of code in .NET 2.0 that will help facilitate our endeavors, there are still some classes that we will have to implement in order to make the picture complete (see Table 2).

Table 2. Supporting elements of the provider framework in .NET 2.0

ClassDescription
System.Configuration.Provider.ProviderBaseThe abstract base class that all feature providers must inherit from.
System.Configuration.Provider.ProviderCollectionA collection of providers. Should be subclassed as a provider-specific collection for individual features.
System.Web.ProvidersHelperInstantiates a provider or collection of providers, using settings from the application's configuration file.
System.Configuraion.ProviderSettingsRepresents a group of settings that are added to the <providers> element within a configuration section. Inherits from Configuration Element.
System.Configuration.Provider.ProviderExceptionThrown when a configuration provider error has occurred.

There are three types of tasks that we will have to perform in order to implement our custom provider:

  • Framework Extensions—Work that you need to do once, in order to make the existing .NET provider framework classes useful to you. (See Table 3.)
  • Feature Implementation—Work that you need to do when making a new feature that leverages the provider infrastructure. (See Table 4.)
  • Provider Implementation—Work that you need to do every time you make a specific provider that can service your feature. (See Table 5.)

Table 3. Framework extensions: Items you need to implement once to extend the existing provider framework

ClassDescription
ProviderFeatureSectionAbstract class derived from System.Configuration.ConfigurationSection that represents the base structure for a configuration section that will be used for a specific provider-based function. (Implement this once.)

Table 4. Feature implementation: Items to implement when making a new provider-based feature

ClassDescription
<CustomFunctionName>Sealed abstract class with static properties, methods, and events for the API for your custom functionality.
<CustomFunctionName>ProviderAbstract class with abstract public properties, methods, and events that either echo the public API defined in <CustomFunctionName> or support public members in peripheral objects.
<CustomFunctionName>ConfigurationSectionDerived from ProviderFeatureSection. Represents the configuration structure for a provider-based custom function. (Must be implemented for every new provider feature that you build.)
<CustomFunctionName>ProviderCollectionA collection of function-specific providers. (Must be implemented for every new provider feature you build.)

Table 5. Provider implementation: Items to implement when making a new provider-based feature

ClassDescription
<Implemenation><CustomFunctionName>ProviderRepresents the configuration structure for a provider-based custom function. (Must be implemented for every new technology-specific implementation of the functionality that you build.)

Once we have taken care of the prerequisites, we can proceed with the tasks that are needed in order to implement our provider-based feature. The following is an overview of what needs to be done (see also Figure 1).

Outline for creating a custom provider based feature in .NET 2.0

  1. Design the API and object model for your specific unit of functionality. Create an abstract class as the focal point for your feature, as well as any peripheral classes in your object model that your individual API needs to work on or deal with. The name for your abstract class should be <CustomFunctionName>.
  2. Derive an abstract class from System.Configuration.Provider.ProviderBase that supports the API that you designed in Step 1. Many of the members may simply echo the public members of the <CustomFunctionName> API class, or directly support members in other peripheral classes used by your API. The name for this class should be <CustomFunctionName>Provider.
  3. Derive a concrete class from System.Configuration.Provider.ProviderCollection that implements a type-specific collection for your provider-based function. The name for this class should be <CustomFunctionName>ProviderCollection.
  4. Create a concrete class that inherits from the abstract <CustomFunctionName>Provider class that you authored in Step 2. This will be the first concrete provider for your feature. You must implement all of the required members of this class. The name for this class should be <TechnologyName><CustomFunctionName>Provider.

Click here for larger image

Figure 1. Static diagram of a provider pattern implementation in .NET for a feature (Click on the image for a larger picture)

We will explore a sample implementation for a new provider-based feature that implements functionality for retrieving and managing pricing information within our enterprise.

First Things First

If this is the first provider-based feature that you are building, you will need to lay some groundwork, as we mentioned earlier, in order to extend the .NET 2.0 provider framework just a bit. Our first task is to take care of the need for applications that use your feature to specify configuration information within the web.config or app.config file for the feature. Part of this configuration information includes identifying the providers that you expect to use, and the assembly that contains each provider's implementation. We will create a reusable base class called ProviderFeatureSection that leverages the new configuration approach in .NET 2.0 (see Listing 1).

Listing 1. Implementation of the ProviderFeatureSection class

public abstract class ProviderFeatureSection : System.Configuration.ConfigurationSection
    {
private readonly ConfigurationProperty defaultProvider = new ConfigurationProperty("defaultProvider", typeof(string), null);

private readonly ConfigurationProperty providers = new ConfigurationProperty("providers", typeof(ProviderSettingsCollection), null);

private ConfigurationPropertyCollection properties = new ConfigurationPropertyCollection();

        public ProviderFeatureSection()
        {
            properties.Add(providers);
            properties.Add(defaultProvider);
        }

        [ConfigurationProperty("defaultProvider")]
        public string DefaultProvider
        {
            get { return (string)base[defaultProvider]; }
            set { base[defaultProvider] = value; }
        }

        [ConfigurationProperty("providers")]
        public ProviderSettingsCollection Providers
        {
            get { return (ProviderSettingsCollection)base[providers]; }
        }

        protected override ConfigurationPropertyCollection Properties
        {
            get { return properties; }
        }
    }

Creating this class allows us to easily build specialized support for sections in our configuration file that look like the one shown in Listing 2 for configuring pricing. This specific example uses a class called PricingConfigurationSection that has been derived from our generic ProviderFeatureSection. It contains entries for two types of PricingProviders: an SAPPricingProvider and an SQLPricingProvider.

Listing 2. Configuration file excerpt for example pricing functionality

<?xml version="1.0"?>
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
   <configSections>
      <section name="pricing" type="Pricing.PricingConfigurationSection, Pricing,            Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c61934e088">
      </section>
   </configSections>
   <pricing defaultProvider="SAPPricingProvider">
      <providers>
      <add name="SAPPricingProvider" ConnectionString="conn_string_here"></add>
      <add name="SQLPricingProvider" ConnectionString="conn_string_here"></add>
      </providers>
   </pricing>
</configuration>

Step 1: Design and Implement the Public Interface (API) for Your Feature

In the example solution, we want to retrieve and manage pricing information by connecting potentially to several different back-end systems in our organization. One way to accomplish this is to identify the common business functionality proffered in each system, and craft a common API. Of course, this means we could be sacrificing some functionality that is available in our more advanced pricing systems, simply because there is no analog for it present in other older or less-capable systems. Fortunately, there are some strategies for using this pattern even when some back-end providers don't match up well to others. These include:

  • Providing public properties in your abstract provider interface, and in your API, that allow client applications to query the availability of features in the currently configured provider. A good example of this is in the EnablePasswordReset and RequiresQuestionAndAnswer properties of the Membership API. Both are read-only properties that return a Boolean value indicating whether the currently configured default provider allows or has the ability to service password reset and question and answer challenges.
  • Using "glue" code in your provider implementation to close the gaps between the more advanced providers in your organization and the less capable ones. Essentially, you would implement functionality that the back-end system is missing within your provider.

For our example, the public API that we have designed is simple, and focuses on exhibiting common qualities of the provider pattern. Our focal point is the abstract Pricing class, and we have one peripheral class named PriceQuote. All of the classes that participate in forming our API should have implementations that can vary independently of the back-end providers that might receive messages from them. Our abstract focal point class should have some static read-only properties to retrieve values for common configuration parameters of the currently configured provider, and static methods and events that define and expose the service actions offered. It should also have Provider and Providers properties that return the default configured provider and all configured providers, respectively. Peripheral classes will need a ProviderName property that directly links back to the name of the provider that is responsible for creating them. This is necessary because these objects may contain methods that call the corresponding methods on the provider class identified by ProviderName. The methods and peripheral classes central to our pricing feature are shown in Figure 2, and described in Table 6 and Table 7.

Click here for larger image

Figure 2. Methods and peripheral classes for the pricing feature (Click on the image for a larger picture)

Table 6. Members of the Pricing class

MemberDescription
ProvidersReturns a collection of the pricing providers that are configured for the application.
ProviderReturns a reference to the default PricingProvider configured for the application.
EnableUpdatesGets a value indicating whether the current pricing provider is configured to, or able to, allow pricing updates.
GetPricingReturns a reference to a PriceQuote object that reflects pricing for the specified product within the context of a specific country and/or customer.
UpdatePricingUpdates the pricing provider's data store with the specified PriceQuote object.
CreatePricingPersists data from a PriceQuote object into the provider's data store.
Initialize (private member)Reads information from configuration file to instantiate the configured providers. This method needs to be called prior to calls to functionality methods such as GetPricing or UpdatePricing.

Table 7. Members of the PriceQuote class

MemberDescription
ProviderNameGets the name of the pricing provider that is responsible for the information in this PriceQuote object.
ProductSkuThe product sku identifier from the pricing data source for this PriceQuote object.
IsoCountryThe ISO country code context for this PriceQuote object.
PublishedIndicates whether this PriceQuote is publicly available information and whether it should be published into catalogs.
UpdatePublicationStatusInvokes the pricing provider that created this PriceQuote object to change the publication status for this object.

The Pricing API class will not directly implement the functionality necessary to handle pricing tasks, but will defer to the providers that are configured within the application's web.config or app.config file. The Initialize method contains the logic necessary in order to read the configuration file and instantiate the providers defined there. Inside this method you will need to do the following (see Listing 3):

  1. Determine whether providers have already been initialized.
  2. Retrieve the ConfigurationSection for the provider-based feature.
  3. Create a ProviderCollection, and populate it with references to the instantiated configured providers using the System.Web.Configuration.ProvidersHelper class.
  4. Store a reference to the default provider specified in the application's configuration file.

Listing 3. Implementation of the Initialize method within the Pricing class

private static void Initialize()
   {
       PricingConfigurationSection pricingConfig = null;

       // don't initialize providers more than once
       if (!_initialized)
       {

         // get the configuration section for the feature
         pricingConfig =             (PricingConfigurationSection)ConfigurationManager.GetSection("pricing");
                                
        if (pricingConfig == null)
               throw new Exception("Pricing is not configured for this application");                                    
                
        _providers = new PricingProviderCollection();
    
    // use the ProvidersHelper class to call Initialize on each 
    // configured provider
        ProvidersHelper.InstantiateProviders(pricingConfig.Providers, _providers,    typeof(PricingProvider));
            
    // set a reference to the default provider
        _provider = _providers[pricingConfig.DefaultProvider];

    // set this feature as initialized
        _initialized = true;
         
   }

Step 2: Extend from ProviderBase

Now we will create PricingProvider, an abstract class that defines the interface to which all of our "pluggable" providers that perform pricing actions must conform. Keeping in line with the specification, we will create this abstract class by extending from ProviderBase. It should contain public abstract properties that hold configured values for feature-specific providers. and public abstract methods that will form the template for the implementation of your custom functionality in derived classes (see Listing 4). In general, this class will have five types of members (see Table 8):

  1. Members inherited from ProviderBase.
  2. Members that are identical to members in the public API that we defined in Step 1.
  3. Members that lend support to "façade" properties and methods in the public API, or specific methods within our peripheral objects.
  4. Properties intended to hold generic configuration settings for providers.
  5. Events that allow subscription to actions that a provider can perform.

Table 8. Members of the PricingProvider class

MemberMember TypeDescription
Name1Inherited from ProviderBase.
Description1Inherited from ProviderBase.
Initialize1Inherited from ProviderBase.
ApplicationName4Abstract get/set property that holds the name of the application using pricing functionality.
EnableUpdates4Abstract read-only property indicating whether a provider is able to, or is configured to, affect updates to the underlying data source for pricing.
GetPricing2Returns a reference to a PriceQuote object that reflects pricing for the specified product within the context of a specific country and/or customer.
CreatePricing2Persists data from a PriceQuote object into the provider's data store.
PublishPricing3Changes the publication status of an existing PriceQuote to a published state.
UnPublishPricing3Changes the publication status of an existing PriceQuote to an unpublished state.
UpdatePricing2Updates the pricing provider's data store with the specified PriceQuote object.

Listing 4. Implementation of the abstract PricingProvider class

public abstract class PricingProvider : ProviderBase
    {
        public abstract string ApplicationName { get; set;}

        public abstract bool EnableUpdates { get;}

        public abstract PriceQuote GetPricing(string productSku,
            string isoCountry);
        
        public abstract PriceQuote CreatePricing(string productSku, string isoCountry,
            decimal price);
 
        public abstract void UpdatePricing(PriceQuote pricingInfo);

        public abstract void PublishPricing(string productSku, string isoCountry);

        public abstract void UnPublishPricing(string productSku, string isoCountry);

    }

Step 3: Derive from ProviderCollection

Our concrete PricingProviderCollection class serves as the type specific PricingProvider collection. An object of this type will be used in the Pricing.Providers property. In order to create this class, you should derive from the existing ProviderCollection class and override the methods that you will use to add specific items to the collection. We need to define our own indexer method and override the Add method, as shown in Listing 5.

Listing 5. Implementation of the type-specific PricingProviderCollection class

public class PricingProviderCollection : ProviderCollection
    {
        public override void Add(ProviderBase provider)
        {       
            string providerTypeName;

            // make sure the provider supplied is not null
            if (provider == null)
                throw new ArgumentNullException("provider");

            if (provider as PricingProvider == null)
            {
                providerTypeName = typeof(PricingProvider).ToString();
                throw new ArgumentException("Provider must implement PricingProvider         type", providerTypeName);
            }
            base.Add(provider);
        }

        
        new public PricingProvider this[string name] {
            get
            {
                return (PricingProvider)base[name];
            }
   }
        
    }

Step 4: Implement a Provider

Now let's create our concrete provider. Since this class will be the first concrete class in the inheritance hierarchy that begins with ProviderBase, there are several key members of the ProviderBase class that should be overridden. These include the Name property, the Description property, and the Initialize method.

While in reality it is likely that you will have several providers for each functional aspect of your enterprise, for our example, we will implement a single provider by deriving a class called SqlPricingProvider from the PricingProvider class that we created in Step 2. It is conceivable that we could also implement an SapPricingProvider or a SiebelPricingProvider, and so on. To stay in line with the specification, remember to name your class using the pattern <TechnologyName><CustomFunctionName>Provider.

For our example, we will mock an interface to an SQL-driven pricing repository. Because this is the first concrete class in its inheritance hierarchy, there are several members that should be overridden to reflect the specificity of the implementation. The actual functionality related to retrieving, updating, and saving pricing from an SQL datastore would be implemented here.

Specific attention should be given to overriding the Initialize method. This method is called from the Pricing.Initialize method when the ProvidersHelper is invoked to instantiate all configured providers, and it is where all of the "heavy lifting" occurs. It is a factory method specialized for use in the .NET 2.0 provider framework. The responsibilities of this method are to set up the internal state of the SqlPricingProvider according to what is specified in the application's configuration file, using the NameValueCollection passed in by the ProvidersHelper. This could include setting configuration properties such as whether updates are enabled for this provider, or caching connection strings, and so on. The Initialize method, and one of the methods providing functionality for creating pricing, is shown in Listing 6 and Listing 7, respectively.

Listing 6. Implementation of the Initialize method inside the SqlPricingProvider class

public override void Initialize(string name, NameValueCollection config)
        {
            // set the Provider name
            if (String.IsNullOrEmpty(name))
                name = "SqlPricingProvider";

            // make sure configuration parameters were passed in
            if (config == null)
              throw new ArgumentNullException("config");
            
            // set the Provider description
           if (String.IsNullOrEmpty(config["description"])) {
              config.Remove("description");
              config.Add("description", "Sql Server Based Pricing Provider");
           }
            
            base.Initialize(name, config);

            // set whether or not updates are enabled
            // depending on the provider you may want to 
            // ignore what is in the config file
            // or 
            if (String.IsNullOrEmpty(config["enableUpdates"]))
                this._enableUpdates = false;
            else
                this._enableUpdates = Boolean.Parse(config["enableUpdates"]);

            this._appName = config["applicationName"];

            // for Providers that use connection strings 
            // leverage the benefits of the new .NET connection
            // string configuration section by looking up the connection
            // string identified by connectionStringName within the provider 
            // section
            string connectionStringName = config["connectionStringName"];
            
            if (string.IsNullOrEmpty(connectionStringName))
                throw new ProviderException("Connection string name is not specified");
            
            // get the Connection String from the ConnectionStrings Config Section
            this._sqlConnectionString = ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString;
            
            // make sure the connection string is actually there
            if (string.IsNullOrEmpty(this._sqlConnectionString))
                throw new ProviderException("Connection string is not found");
                     
        }

Listing 7. Sample implementation of a method inside SqlPricingProvider

public override PriceQuote CreatePricing(string productSku, string isoCountry, 
   decimal price)
       {
            if (!(this.EnableUpdates))
                throw new NotSupportedException("Not Configured to support pricing         updates");

            // Actual implementation omitted for simplicity
            // normally you would connect to your database 
            // and execute a stored procedure etc.

            PriceQuote pricingInfo = new PriceQuote(this.Name, productSku, isoCountry,    price);

            return pricingInfo;            
        }

The following is an example of how we might actually use our provider-based feature.

Listing 8

class Test
    {
        static void Main(string[] args)
        {
            PriceQuote testQuote = Pricing.GetPricing("123456-999", "US");
            
            Console.WriteLine(testQuote.ToString());
            Console.ReadLine();

            
        }
    }

Conclusion

We have created a fully functional provider-based pricing system that can easily be extended by adding new providers as needed. We used the built-in classes that form the foundation of the other provider-based features that we find sprinkled throughout .NET 2.0, and leveraged the utility of the configuration system and the CLR type loader.

Show: