Creating Your Own Provider Framework

posted on 02/20/08 at 12:53:51 am by Joel Ross

The Provider pattern is used in quite a few different locations in the .NET Framework (2.0). It's used for Membership, Roles, SiteMaps, and for encrypted configuration file sections. If you search around for help on them, you'll find quite a bit - but they are all pretty much about how to create your own provider for one of the already pre-built providers. There isn't much out there for framework developers who want to add a provider model to their own application.

As part of the NuSoft Framework, we went down the road of doing validation through a provider model, and I actually had a full-blown implementation of it that we're going to throw out. But before I do that, I wanted to document (for future reference) exactly what it takes to build your own provider framework.

There are 5 main pieces to create your own provider. I'll start with a high level view of what you need, walk through an example, and then dive into the details of each piece.

  1. Your abstract base provider class. This inherits from ProviderBase. All providers should be based on this class, so your custom one will have this as well.
  2. Your static provider class. This is what all end users will use. Internally, this will call the correct provider.
  3. Your provider configuration class. There's a configuration portion to most providers, and you can add custom information in there as well, so you'll need a custom configuration section.
  4. A provider helper class. This helps with the loading of your providers. If you know it's being used in a web application, there's a great one in the System.Web DLL. If you're not sure your provider will be used in a web site, then you probably don't want to reference System.Web just to get the helper class. I'll lay out what's in the helper so you don't need to rely on System.Web.
  5. A concrete provider. This is optional, but most providers supply at least one implementation of their abstract base provider class. It's up to you, and I'll show how you can be safe even if a provider isn't supplied.

As an example, let's look at Membership. There's an abstract class called MembershipProvider that defines the methods you'd need to implement if you were to make your own membership provider. Unless you do implement your own, you might never know about this class, because all client code calls the static Membership class - Membership.GetUser(), Membership.CreateUser(), etc.

To specify a membership provider, you Google for the config section you need, and add that. Most people use the SqlMembershipProvider, but there's also a MySqlMembershipProvider, and I think an OracleMembershipProvider. All of those classes are concrete implementations of the abstract MembershipProvider class.

So how does all of this work? Let's get into code, so you can see what needs to happen.

First, you have to define your contract - your abstract base class. Any method that you want your provider to handle should be included in this class.

   1:  public abstract class DataProvider : ProviderBase
   2:  {
   3:      public abstract void Get();
   4:      public abstract void Delete();
   5:  }

This is obviously not a real example, but the main thing to notice is 1.) It inherits from System.Configuration.Provider.ProviderBase, and 2.) no implementation is provided - all methods are abstract.

Next up is the static class that client code will use. The name is typically the same as the abstract class, minus the Provider part. Sticking to our (fake) example. we'd have a static class called Data.

   1:  public static class Data
   2:  {
   3:      private static bool _isInitialized = false;
   4:   
   5:      private static DataProvider _provider;
   6:      public static DataProvider Provider
   7:      {
   8:          get
   9:          {
  10:              Initialize();
  11:              return _provider;
  12:          }
  13:      }
  14:   
  15:      private static DataProviderCollection _providers;
  16:      public static DataProviderCollection Providers
  17:      {
  18:          get
  19:          {
  20:              Initialize();
  21:              return _providers;
  22:          }
  23:      }
  24:   
  25:      private static void Initialize()
  26:      {
  27:          DataProviderConfigurationSection dataConfig = null;
  28:   
  29:          if (!_isInitialized)
  30:          {
  31:              // get the configuration section for the feature
  32:              dataConfig = (DataProviderConfigurationSection)ConfigurationManager.GetSection("data");
  33:   
  34:              if (dataConfig == null)
  35:              {
  36:                  throw new ConfigurationErrorsException("Data is not configured to be used with this application");
  37:              }
  38:   
  39:              _providers = new DataProviderCollection();
  40:   
  41:              // use the ProvidersHelper class to call Initialize() on each provider
  42:              ProvidersHelper.InstantiateProviders(dataConfig.Providers, _providers, typeof(DataProvider));
  43:   
  44:              // set a reference to the default provider
  45:              _provider = _providers[dataConfig.DefaultProvider] as DataProvider;
  46:   
  47:              _isInitialized = true;
  48:          }
  49:      }
  50:   
  51:      public static void Get()
  52:      {
  53:          Initialize();
  54:          if (_provider != null)
  55:          {
  56:              _provider.Get();
  57:          }
  58:      }
  59:   
  60:      public static void Delete()
  61:      {
  62:          Initialize();
  63:          if (_provider != null)
  64:          {
  65:              _provider.Delete();
  66:          }
  67:      }
  68:  }

There's a few things to note in this class. First, it has static methods for each method in the abstract provider class (Get and Delete). The internals of those calls all call Initialize, then checks if the provider isn't null, and if it's not, then makes the call. The initialize method is meant to only run once - it uses a variable to see if it's already been initialized, and if not, then it reads your configuration and loads the providers you've specified in the config. It uses a ProvidersHelper static class to do the actual loading, which we'll show in a bit. Once it loads all of the providers, it sets the default one that you specify, which is then used in the original call.

The code above expects that a configuration will be added to your application's config file, and if not, it will throw an exception. For something like data access, membership, etc., that may be the right call. For something like validation or an optional action, that may not be the best course. In that case, you could add a try/catch around the loading of the configuration (the Initialization method), and then either add a default provider implementation or have a default action in your static methods (it would be the else on if(_provider != null)).

While we're on configuration, we should look at the config section that would have to be added to the config file.

   1:  <configuration>
   2:     <configSections>
   3:        <section name="data" type="DataProviderConfigurationSection" />
   4:     </configSections>
   5:     <data defaultProvider="MyDataProvider">
   6:        <providers>
   7:           <add name="MydataProvider" type="MyDataProvider"  />
   8:        </providers>
   9:     </data>
  10:  </configuration>

In our Data static class, our initialize was looking for a section called data. In the config file, we need to define that section (the <configSections> element, and tell it what configuration section provider to use. Then we add the <data> section, specifying the list of providers and which one to use as the default. If you've looked at the Initialize method above, you'll see that we have a custom configuration section class.

   1:  public class DataProviderConfigurationSection : ConfigurationSection
   2:  {
   3:      public DataProviderConfigurationSection()
   4:      {
   5:          _defaultProvider = new ConfigurationProperty("defaultProvider", typeof(string), null);
   6:          _providers = new ConfigurationProperty("providers", typeof(ProviderSettingsCollection), null);
   7:          _properties = new ConfigurationPropertyCollection();
   8:   
   9:          _properties.Add(_providers);
  10:          _properties.Add(_defaultProvider);
  11:      }
  12:   
  13:      private readonly ConfigurationProperty _defaultProvider;
  14:      [ConfigurationProperty("defaultProvider")]
  15:      public string DefaultProvider
  16:      {
  17:          get { return (string)base[_defaultProvider]; }
  18:          set { base[_defaultProvider] = value; }
  19:      }
  20:   
  21:      private readonly ConfigurationProperty _providers;
  22:      [ConfigurationProperty("providers")]
  23:      public ProviderSettingsCollection Providers
  24:      {
  25:          get { return (ProviderSettingsCollection)base[_providers]; }
  26:      }
  27:   
  28:      private ConfigurationPropertyCollection _properties;
  29:      protected override ConfigurationPropertyCollection Properties
  30:      {
  31:          get { return _properties; }
  32:      }
  33:  }

It's pretty straightforward, but it helps define that we have a collection of providers and a defaultProvider property. You can add your own custom properties if you want - for example, for a data provider, you might want to specify either a connection string or the name of an existing connection string already specified. For this simple example, I don't have any of those.

I've already shown the usage of the ProvidersHelper class, but for completeness, I'll include it as well.

   1:  public static class ProvidersHelper
   2:  {
   3:      private static Type providerBaseType = typeof(ProviderBase);
   4:   
   5:      /// <summary>
   6:      /// Instantiates the provider.
   7:      /// </summary>
   8:      /// <param name="providerSettings">The settings.</param>
   9:      /// <param name="providerType">Type of the provider to be instantiated.</param>
  10:      /// <returns></returns>
  11:      public static ProviderBase InstantiateProvider(ProviderSettings providerSettings, Type providerType)
  12:      {
  13:          ProviderBase base2 = null;
  14:          try
  15:          {
  16:              string str = (providerSettings.Type == null) ? null : providerSettings.Type.Trim();
  17:              if (string.IsNullOrEmpty(str))
  18:              {
  19:                  throw new ArgumentException("Provider type name is invalid");
  20:              }
  21:              Type c = Type.GetType(str, true, true);
  22:              if (!providerType.IsAssignableFrom(c))
  23:              {
  24:                  throw new ArgumentException(String.Format("Provider must implement type {0}.", providerType.ToString()));
  25:              }
  26:              base2 = (ProviderBase)Activator.CreateInstance(c);
  27:              NameValueCollection parameters = providerSettings.Parameters;
  28:              NameValueCollection config = new NameValueCollection(parameters.Count, StringComparer.Ordinal);
  29:              foreach (string str2 in parameters)
  30:              {
  31:                  config[str2] = parameters[str2];
  32:              }
  33:              base2.Initialize(providerSettings.Name, config);
  34:          }
  35:          catch (Exception exception)
  36:          {
  37:              if (exception is ConfigurationException)
  38:              {
  39:                  throw;
  40:              }
  41:              throw new ConfigurationErrorsException(exception.Message, 
  42:                  providerSettings.ElementInformation.Properties["type"].Source, 
  43:                  providerSettings.ElementInformation.Properties["type"].LineNumber);
  44:          }
  45:          return base2;
  46:      }
  47:      
  48:      public static void InstantiateProviders(ProviderSettingsCollection providerSettings, ProviderCollection providers, Type type)
  49:      {
  50:          foreach (ProviderSettings settings in providerSettings)
  51:          {
  52:              providers.Add(ProvidersHelper.InstantiateProvider(settings, type));
  53:          }
  54:      }
  55:  }

There's a lot to this class, but it's something that you can copy and paste into your code base and use as-is. Basically, it looks at the config section, and finds providers you've added and dynamically loads them into memory. This works across assemblies - so if your provider is in DLL 1 and your provider implementation is in another, it will still load (assuming you specify the DLL in the type. It follows the standard format for that - type="My.Dll.Namespace.Class, My.Dll.Name").

The last piece is where you'll find the most help out there - implementing the concrete class for a provider.

   1:  public class MyDataProvider : DataProvider
   2:  {
   3:      public override void Get()
   4:      {
   5:          // Get Code
   6:      }
   7:   
   8:      public override void Delete()
   9:      {
  10:          // Delete Code
  11:      } 
  12:  }

Again, this isn't a real implementation, so there's no real code here, but the idea is that your class inherits from your abstract class, and you implement the methods you've defined. I didn't have a need to, but in ProviderBase, there's a method called Initialize that gets called by the ProvidersHelper above. It allows you to grab any custom properties from your config sections to use in your provider.

You may have noticed there's a class that was mentioned above, but I never defined. In our static Data class, we have a collection of DataProviders, and it's type is DataProviderCollection. This is a simple class, so I didn't really mention it, but if you do your own, you'll want to know about it.

   1:  public class DataProviderCollection : ProviderCollection { }

The ProviderCollection is in the framework, and you can override methods in it to handle how providers are added to the collection.

With the above code, you can take that and implement your own provider for your own application. In a follow up post, I plan on showing how you can implement your own data provider as a way to cement the concept a bit more.

Tags: |

Categories: Development, C#