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 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.

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

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>

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

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.

© Microsoft Corporation. All rights reserved.