2008-02-22

Custom config sections

The website I am writing will sell some software I have already written. In addition it will sell "Collateral", which is basically support files for the software. The software itself will only run if it finds a license, which is an RSA signed binary file containing information such as the email address of the licensee. In addition some kinds of collateral will also be RSA signed with the licensee’s email address so that it will only work for that user, but not all collateral types are signed, for example a Character is a custom file format and is signed but a WAV file will not be signed.

So this website needs to sell software + provide a license. It also needs to sell collateral, some of which will require signing and some of which will not.

Software and Collateral are both types of Product, and you can buy a Product. The problem is how should I deal with the 3 different types of licensing (license file, signed binary, no license)? In addition to this should I really create a concrete descendant of the "Software" class just to implement a way of providing a license? Erm, no!

If I were to add a concrete class for every type of product it would mean I have to change the model for the website everytime a new product was released, what a pain! From the bottom-up this is what I did.

01: A separate assembly MyWebSite.Licensing with the following two interfaces in it (Edition is linked to Software, so we can buy a license for different editions, Std, Pro, etc):

public interface ISoftwareLicenseGenerator
{
  byte[] Generate(Edition edition, CustomerRole customer);
  string FileExtension { get; }
}

public interface ICollateralLicenseGenerator
{
  byte[] Generate(Collateral collateral, CustomerRole customer);
  string FileExtension { get; }
}


02: Another separate assembly with implementations for the interfaces in them. This assembly is then named something like "MyWebSite.Licensing.SoftwareName".

03: In the web.config file I can then add the following:

<licenseGenerators>
  <software>
    <add uniqueID="SoftwareName" type="MyNameSpace.SoftwareLicenseGeneratorClassName, NameOfAssembly"/>
  </software>
  <collateral>
    <add uniqueID="CollateralTypeName" type="MyNameSpace.CollateralLicenseGeneratorClassName, NameOfAssembly"/>
  </collateral>
</licenseGenerators>


This is parsed when the application is first run, giving me access to a list of generators available which I keep in a Dictionary<string, ......> so that I can look them up by name. If you directly add this kind of section to your web.config your app will not start, so I thought I’d show how to add custom sections + access their values from code....


01: Create a descendant of System.Configuration.ConfigurationSection

public class LicenseGeneratorsSection : ConfigurationSection
{
  public LicenseGeneratorsSection()
  {
  }

  [ConfigurationProperty("software")]
  public SoftwareLicenseGeneratorElementCollection SoftwareLicenseGenerators
  {
    get
    {
      return (SoftwareLicenseGeneratorElementCollection)this["software"];
    }
  }

  [ConfigurationProperty("collateral")]
  public CollateralLicenseGeneratorElementCollection CollateralLicenseGenerators
  {
    get
    {
      return (CollateralLicenseGeneratorElementCollection)this["collateral"];
    }
  }
}

This defines a section with no attributes + 2 element collections, one named "software" and one named "collateral". Next a class is needed for each which descends from System.Configuration.ConfigurationElementCollection so that System.Configuration knows what kind of object to add to these collections when we do <add ......./> within <software> or <collateral>.


02: Element collection, I will list the source only for one...

public class SoftwareLicenseGeneratorElementCollection : ConfigurationElementCollection
{
  //Creates a new instance for the xml <add uniqueID="X" type="Y"/>
  protected override ConfigurationElement CreateNewElement()
  {
    return new SoftwareLicenseGeneratorElement();
  }

  //Gets the key value, in this case the UniqueID property, this avoids duplicates
  protected override object GetElementKey(ConfigurationElement element)
  {
    return ((SoftwareLicenseGeneratorElement)element).UniqueID;
  }
}


03: Finally (for the config settings classes) a class is needed to define the attributes UniqueID and Type.

public class SoftwareLicenseGeneratorElement : ConfigurationElement
{
  public SoftwareLicenseGeneratorElement()
  {
  }

  [ConfigurationProperty("uniqueID", IsRequired = true, IsKey = true)]
  public string UniqueID
  {
    get { return (string)this["uniqueID"]; }
    set { this["uniqueID"] = value; }
  }

  [ConfigurationProperty("type", IsRequired = true)]
  public string Type
  {
    get { return (string)this["type"]; }
    set { this["type"] = value; }
  }
}

04: But before just putting the custom XML into web.config you need to declare this section and associate it with the new ConfigurationSection class

<configSections>
  <section name="licenseGenerators" type="MyWebsite.Configuration.LicenseGeneratorsSection, MyWebsite"/>
</configSections>

this tells ASP .NET to expect a custom section named "licenseGenerators" and to use the LicenseGeneratorsSection class in MyWebsite.dll to determine the structure of it, here it is again:


<licenseGenerators>
  <software>
    <add uniqueID="SoftwareName" type="MyNameSpace.SoftwareLicenseGeneratorClassName, NameOfAssembly"/>
  </software>
  <collateral>
    <add uniqueID="CollateralTypeName" type="MyNameSpace.CollateralLicenseGeneratorClassName, NameOfAssembly"/>
  </collateral>
</licenseGenerators>



To read the config section at runtime is really simple

LicenseGeneratorsSection licenseGeneratorsSection;
licenseGeneratorsSection =
  (LicenseGeneratorsSection)ConfigurationManager.GetSection("licenseGenerators");


Here is the entire class source for the static LicenseGenerator class

public static class LicenseGenerator
{
  static Dictionary<string, ISoftwareLicenseGenerator> SoftwareLicenseGenerators = new Dictionary<string, ISoftwareLicenseGenerator>();
  static Dictionary<string, ICollateralLicenseGenerator> CollateralLicenseGenerators = new Dictionary<string, ICollateralLicenseGenerator>();

  static LicenseGenerator()
  {
    //Read the web.config
    LicenseGeneratorsSection licenseGeneratorsSection;
    licenseGeneratorsSection =
      (LicenseGeneratorsSection)ConfigurationManager.GetSection("licenseGenerators");

    foreach (SoftwareLicenseGeneratorElement currentElement in licenseGeneratorsSection.SoftwareLicenseGenerators)
      SoftwareLicenseGenerators[currentElement.UniqueID] = (ISoftwareLicenseGenerator)GetInstance(currentElement.Type);

    foreach (CollateralLicenseGeneratorElement currentElement in licenseGeneratorsSection.CollateralLicenseGenerators)
      CollateralLicenseGenerators[currentElement.UniqueID] = (ICollateralLicenseGenerator)GetInstance(currentElement.Type);
  }

  private static object GetInstance(string typeName)
  {
    Type objectType = Type.GetType(typeName);
    return Activator.CreateInstance(objectType);
  }

  public static ICollateralLicenseGenerator GetCollateralLicenseGenerator(string uniqueID)
  {
    return CollateralLicenseGenerators[uniqueID];
  }

  public static string[] GetCollateralLicenseGeneratorIDs()
  {
    List<string> result = new List<string>();
    foreach (KeyValuePair<string, ICollateralLicenseGenerator> kvp in CollateralLicenseGenerators)
      result.Add(kvp.Key);
    result.Sort();
    return result.ToArray();
  }

  public static ISoftwareLicenseGenerator GetSoftwareLicenseGenerator(string uniqueID)
  {
    return SoftwareLicenseGenerators[uniqueID];
  }

  public static string[] GetSoftwareLicenseGeneratorIDs()
  {
    List<string> result = new List<string>();
    foreach (KeyValuePair<string, ISoftwareLicenseGenerator> kvp in SoftwareLicenseGenerators)
      result.Add(kvp.Key);
    result.Sort();
    return result.ToArray();
  }
}



This solution allows me to add new products at runtime without any problems. Any time a new type of license generator is required I just create a new assembly with a class that implements the correct interface and then register it in the web.config file!

No comments: