Custom Provider-Based Services
Introduction to the Provider Model
Membership Providers
Role Providers
Site Map Providers
Session State Providers
Profile Providers
Web Event Providers
Web Parts Personalization Providers
Custom Provider-Based Services
Hands-on Custom Providers: The Contoso Times
ASP.NET 2.0 includes a number of provider-based services for reading, writing, and managing state maintained by applications and by the run-time itself. Developers can write services of their own to augument those provided with the system. And they can make those services provider-based to provide the same degree of flexibility in data storage as the built-in ASP.NET providers.
There are three issues that must be addressed when designing and implementing custom provider-based services:
- How to architect custom provider-based services
- How to expose configuration data for custom provider-based services
- How to load and initialize providers in custom provider-based services
The sections that follow discuss these issues and present code samples to serve as a guide for writing provider-based services of your own.
Architecting Custom Provider-Based Services
A classic example of a custom provider-based service is an image storage and retrieval service. Suppose an application relies heavily on images, and the application's architects would like to be able to target different image repositories by altering the application's configuration settings. Making the image service provider-based would afford them this freedom.
The first step in architecting such a service is to derive a class from ProviderBase and add abstract methods that define the calling interface for an image provider, as shown in Listing 1. While you're at it, derive a class from ProviderCollection to encapsulate collections of image providers. In Listing 1, that class is called ImageProviderCollection.
Listing 1. Abstract base class for image providers
public abstract class ImageProvider : ProviderBase { // Properties public abstract string ApplicationName { get; set; } public abstract bool CanSaveImages { get; } // Methods public abstract Image RetrieveImage (string id); public abstract void SaveImage (string id, Image image); } public class ImageProviderCollection : ProviderCollection { public new ImageProvider this[string name] { get { return (ImageProvider) base[name]; } } public override void Add(ProviderBase provider) { if (provider == null) throw new ArgumentNullException("provider"); if (!(provider is ImageProvider)) throw new ArgumentException ("Invalid provider type", "provider"); base.Add(provider); } }
The next step is to build a concrete image provider class by deriving from ImageProvider. Listing 2 contains the skeleton for a SQL Server image provider that fetches images from a SQL Server database. SqlImageProvider supports image retrieval, but does not support image storage. Note the false return from CanSaveImages, and the SaveImage implementation that throws a NotSupportedException.
Listing 2. SQL Server image provider
[SqlClientPermission (SecurityAction.Demand, Unrestricted=true)] public class SqlImageProvider : ImageProvider { private string _applicationName; private string _connectionString; public override string ApplicationName { get { return _applicationName; } set { _applicationName = value; } } public override bool CanSaveImages { get { return false; } } public string ConnectionStringName { get { return _connectionStringName; } set { _connectionStringName = value; } } public override void Initialize (string name, NameValueCollection config) { // Verify that config isn't null if (config == null) throw new ArgumentNullException ("config"); // Assign the provider a default name if it doesn't have one if (String.IsNullOrEmpty (name)) name = "SqlImageProvider"; // 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", "SQL image provider"); } // Call the base class's Initialize method base.Initialize(name, config); // Initialize _applicationName _applicationName = config["applicationName"]; if (string.IsNullOrEmpty(_applicationName)) _applicationName = "/"; config.Remove["applicationName"]; // Initialize _connectionString 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); } } public override Image RetrieveImage (string id) { // TODO: Retrieve an image from the database using // _connectionString to open a database connection } public override void SaveImage (string id, Image image) { throw new NotSupportedException (); } }
SqlImageProvider's Initialize method expects to find a configuration attribute named connectionStringName identifying a connection string in the <connectionStrings> configuration section. This connection string is used by the RetrieveImage method to connect to the database in preparation for retrieving an image. SqlImageProvider also accepts an applicationName attribute if provided but assigns ApplicationName a sensible default if no such attribute is present.
Configuring Custom Provider-Based Services
In order to use a provider-based service, consumers must be able to configure the service, register providers for it, and designate which provider is the default. The Web.config file in Listing 3 registers SqlImageProvider as a provider for the image service and makes it the default provider. The next challenge is to provide the infrastructure that allows such configuration directives to work.
Listing 3. Web.config file configuring the image service
<configuration > ... <connectionStrings> <add name="ImageServiceConnectionString" connectionString="..." /> </connectionStrings> <system.web> <imageService defaultProvider="SqlImageProvider"> <providers> <add name="SqlImageProvider" type="SqlImageProvider" connectionStringName="ImageServiceConnectionString"/> </providers> </imageService> </system.web> </configuration>
Simce <imageService> is not a stock configuration section, you must write a custom configuration section that derives from System.Configuration.ConfigurationSection. Listing 4 shows how. ImageServiceSection derives from ConfigurationSection and adds two properties: Providers and DefaultProvider. The [ConfigurationProperty] attributes decorating the property definitions map ImageServiceSection properties to <imageService> attributes. For example, the [ConfigurationProperty] attribute decorating the DefaultProvider property tells ASP.NET to initialize DefaultProvider with the value of the <imageService> element's defaultProvider attribute, if present.
Listing 4. Class representing the <imageService> configuration section
using System; using System.Configuration; public class ImageServiceSection : ConfigurationSection { [ConfigurationProperty("providers")] public ProviderSettingsCollection Providers { get { return (ProviderSettingsCollection) base["providers"]; } } [StringValidator(MinLength = 1)] [ConfigurationProperty("defaultProvider", DefaultValue = "SqlImageProvider")] public string DefaultProvider { get { return (string) base["defaultProvider"]; } set { base["defaultProvider"] = value; } } }
Next, you must register the <imageService> configuration section and designate ImageServiceSection as the handler for it. The Web.config file in Listing 5, which is identical to the one in Listing 3 except for the changes highlighted in bold, makes <imageService> a valid configuration section and maps it to ImageServiceSection. It assumes that ImageServiceSection lives in an assembly named CustomSections. If you use a different assembly name, you'll need to modify the <section> element's type attribute accordingly.
Listing 5. Making <imageService> a valid configuration section and registering ImageServiceSections as the configuration section handler
<configuration >
<configSections>
<sectionGroup name="system.web">
<section name="imageService"
type="ImageServiceSection, CustomSections"
allowDefinition="MachineToApplication"
restartOnExternalChanges="true" />
</sectionGroup>
</configSections>
<connectionStrings>
<add name="ImageServiceConnectionString" connectionString="..." />
</connectionStrings>
<system.web>
<imageService defaultProvider="SqlImageProvider">
<providers>
<add name="SqlImageProvider" type="SqlImageProvider"
connectionStringName="ImageServiceConnectionString"/>
</providers>
</imageService>
</system.web>
</configuration>
Loading and Initializing Custom Providers
The final piece of the puzzle is implementing the image service and writing code that loads and initializes the providers registered in <imageService>'s <providers> element.
Listing 6 contains the source code for a class named ImageService that provides a programmatic interface to the image service. Like ASP.NET's Membership class, which represents the membership service and contains static methods for performing membership-related tasks, ImageService represents the image service and contains static methods for loading and storing images. Those methods, RetrieveImage and SaveImage, call through to the provider methods of the same name.
Listing 6. ImageService class representing the image service
using System; using System.Drawing; using System.Configuration; using System.Configuration.Provider; using System.Web.Configuration; using System.Web; public class ImageService { private static ImageProvider _provider = null; private static ImageProviderCollection _providers = null; private static object _lock = new object(); public ImageProvider Provider { get { return _provider; } } public ImageProviderCollection Providers { get { return _providers; } } public static Image RetrieveImage(int imageID) { // Make sure a provider is loaded LoadProviders(); // Delegate to the provider return _provider.RetrieveImage(imageID); } public static void SaveImage(Image image) { // Make sure a provider is loaded LoadProviders(); // Delegate to the provider _provider.SaveImage(image); } private static void LoadProviders() { // Avoid claiming lock if providers are already loaded if (_provider == null) { lock (_lock) { // Do this again to make sure _provider is still null if (_provider == null) { // Get a reference to the <imageService> section ImageServiceSection section = (ImageServiceSection) WebConfigurationManager.GetSection ("system.web/imageService"); // Load registered providers and point _provider // to the default provider _providers = new ImageProviderCollection(); ProvidersHelper.InstantiateProviders (section.Providers, _providers, typeof(ImageProvider)); _provider = _providers[section.DefaultProvider]; if (_provider == null) throw new ProviderException ("Unable to load default ImageProvider"); } } } } }
Before delegating calls to a provider, ImageService.RetrieveImage and ImageService.SaveImage call a private helper method named LoadProviders to ensure that the providers registered in <imageService>'s <providers> element have been loaded and initialized. LoadProviders uses the .NET Framework's System.Web.Configuration.ProvidersHelper class to do the loading and initializing. One call to ProvidersHelper.InstantiateProviders loads and initializes the registered image providers-that is, the providers exposed through the Providers property of the ImageServiceSection object that LoadProviders passes to ProvidersHelper.InstantiateProviders.
Observe that the ImageService class implements public properties named Provider and Providers that provide run-time access to the default image provider and to all registered image providers. Similar properties may be found in the Membership class and in other ASP.NET classes representing provider-based services.
Click here to continue on to part 9, Hands-on Custom Providers: The Contoso Times