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.
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 mustoverride 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 MustInherit Class ImageProvider Inherits ProviderBase ' Properties Public MustOverride Property ApplicationName() As String Public MustOverride ReadOnly Property CanSaveImages() As Boolean ' Methods Public MustOverride Function RetrieveImage( _ ByVal id As String) As Image Public MustOverride Sub SaveImage( _ ByVal id As String, ByVal image As Image) End Class Public Class ImageProviderCollection : Inherits ProviderCollection Public Shadows ReadOnly Default Property Item( _ ByVal name As String) As ImageProvider Get Return CType(MyBase.Item(name), ImageProvider) End Get End Property Public Overrides Sub Add(ByVal provider As ProviderBase) If provider Is Nothing Then Throw New ArgumentNullException("provider") End If If Not(TypeOf provider Is ImageProvider) Then Throw New ArgumentException ( _ "Invalid provider type", "provider") End If MyBase.Add(provider) End Sub End Class
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 Inherits ImageProvider Private _applicationName As String Private _connectionString As String Public Overrides Property ApplicationName() As String Get Return _applicationName End Get Set(ByVal value As String) _applicationName = Value End Set End Property Public Overrides ReadOnly Property CanSaveImages() As Boolean Get Return False End Get End Property Public Property ConnectionStringName() As String Get Return _connectionString End Get Set(ByVal value As String) _connectionString = value End Set End Property Public Overrides Sub Initialize(ByVal name As String, _ ByVal config As NameValueCollection) ' Verify that config isn't null If config Is Nothing Then Throw New ArgumentNullException("config") End If ' Assign the provider a default name if it doesn't have one If String.IsNullOrEmpty(name) Then name = "SqlImageProvider" 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", "SQL image provider") End If ' Call the base class's Initialize method MyBase.Initialize(name, config) ' Initialize _applicationName _applicationName = config("applicationName") If String.IsNullOrEmpty(_applicationName) Then _applicationName = "/" End If config.Remove("applicationName") ' Initialize _connectionString 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) _ Is 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 ' Throw an exception if unrecognized attributes remain 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 Public Overrides Function RetrieveImage( _ ByVal id As String) As Image ' TODO: Retrieve an image from the database using ' _connectionString to open a database connection Return Nothing End Function Public Overrides Sub SaveImage( _ ByVal id As String, ByVal image As Image) Throw New NotSupportedException() End Sub End Class
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.
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
Imports System Imports System.Configuration Public Class ImageServiceSection Inherits ConfigurationSection <ConfigurationProperty("providers")> _ Public ReadOnly Property Providers() As ProviderSettingsCollection Get Return CType(MyBase.Item("providers"), _ ProviderSettingsCollection) End Get End Property <StringValidator(MinLength := 1), ConfigurationProperty( _ "defaultProvider", _ DefaultValue := "SqlImageProvider")> _ Public Property DefaultProvider() As String Get Return CStr(MyBase.Item("defaultProvider")) End Get Set MyBase.Item("defaultProvider") = Value End Set End Property End Class
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>
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
Imports System Imports System.Drawing Imports System.Configuration Imports System.Configuration.Provider Imports System.Web.Configuration Imports System.Web Public Class ImageService Private Shared _provider As ImageProvider = Nothing Private Shared _providers As ImageProviderCollection = Nothing Private Shared _lock As Object = New Object() Public ReadOnly Property Provider() As ImageProvider Get Return _provider End Get End Property Public ReadOnly Property Providers() As ImageProviderCollection Get Return _providers End Get End Property Public Shared Function RetrieveImage( _ ByVal imageID As Integer) As Image ' Make sure a provider is loaded LoadProviders() ' Delegate to the provider Return _provider.RetrieveImage(imageID) End Function Public Shared Sub SaveImage(ByVal image As Image) ' Make sure a provider is loaded LoadProviders() ' Delegate to the provider _provider.SaveImage(image) End Sub Private Shared Sub LoadProviders() ' Avoid claiming lock if providers are already loaded If _provider Is Nothing Then SyncLock _lock ' Do this again to make sure _provider is still null If _provider Is Nothing Then ' Get a reference to the <imageService> section Dim section As ImageServiceSection = _ CType(WebConfigurationManager.GetSection( _ "system.web/imageService"), _AppDomain ImageServiceSection) ' Load registered providers and point _provider ' to the default provider _providers = New ImageProviderCollection() ProvidersHelper.InstantiateProviders( _ section.Providers, _providers, _ GetType(ImageProvider)) _provider = _providers(section.DefaultProvider) If _provider Is Nothing Then Throw New ProviderException( _ "Unable to load default ImageProvider") End If End If End SyncLock End If End Sub End Class
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.