Het Provider Model in .NET 2.0
Versie 2.0 van het .NET Framework bevat nieuwe functionaliteit in de vorm van providers. Deze providers stellen ons in staat om in applicaties generieke functionaliteit te implementeren en deze implementatie dynamisch te wisselen. Dat wil zeggen, een functie aanroepen, zonder te weten hoe deze functie geïmplementeerd is. En als de implementatie niet geschikt is voor ons toepassing kunnen we deze wisselen voor een andere implementatie zonder aanpassingen in de aanroepende code.
In dit artikel laten we zien wat de fundamenten zijn van het Provider model en hoe je zelf een provider-gebaseerde feature kunt toevoegen aan je applicatie.
Features
In het Provider model van .NET 2.0 wordt ook gerefereerd aan ‘features’. Een feature van het nieuwe framework is bijvoorbeeld Membership, of Health Monitoring. In essentie bestaat een feature uit een logische set van functies, meestal samengepakt in één klasse. Wanneer we het in de rest van dit artikel over features hebben, dan bedoelen we dus een groep functies die logisch gegroepeerd en direct vanuit programmacode in een client gebruikt kunnen worden.
Pattterns
Het Provider model maakt gebruik van een viertal ontwerptechnieken, ook wel patterns. De belangrijkste is de Strategy pattern. In het kort zorgt het Strategy pattern voor de afscherming van de functionaliteit van een feature zodat de implementatie van zo’n feature kan veranderen zonder aanpassingen in de code die gebruik maakt van deze feature.
Strategy Pattern
Wanneer je dit Strategy pattern wil gebruiken in je applicatie is het ontwerp van de feature erg belangrijk. Het moet duidelijk zijn welke functionaliteit zichtbaar is, zodat verschillende implementaties van deze feature in staat zijn deze functionaliteit te leveren. Daarnaast is het essentieel dat de ene implementatie niet additionele afhankelijkheden oproept die vanuit de programmacode van de client ingevuld moeten worden. Denk hierbij bijvoorbeeld aan een connectiestring, een gebruikersnaam, wachtwoorden e.d.
Factory Method
Het Strategy pattern zou niet zo praktisch zijn als we niet in staat waren eenvoudig te wisselen tussen de ene en de andere implementatie. De Factory method is daarom een logische toevoeging om het principe van Providers te laten werken. De gedachte achter de Factory method is dat er een scheiding moet zijn tussen de instantiatie van klassen en de feature die er gebruik van maakt. Andersgezegd, er moeten geen harde verwijzingen zijn naar de concrete types in de client.
Deze losse koppeling kan geïmplementeerd worden door het gebruik van interfaces of abstracte klassen. De feature-klasse kan zodoende de methoden uit de interface of de abstracte klassen aanspreken zonder te weten hoe deze geïmplementeerd is. Maar we moeten wel ergens vastleggen welke feitelijke klasse de gewenste functie zal uitvoeren.
Gelukkig beschikt het .NET Framework over allerlei mogelijkheden om vanuit een string-waarde een bepaalde klasse in een assembly op te zoeken. Dit heet ook wel ‘reflection’. In combinatie met de configuratiemogelijkheden in XML bestanden die bij een applicatie horen kunnen we de definitie van de feature ‘Provider’ vastleggen en eenvoudig veranderen. Hoe dat precies in zijn werk gaat zien we later in het verhaal.
Singleton Pattern
Een derde ontwerptechniek die van belang is voor het Provider model is het Singleton pattern. Deze gebruiken we wanneer het belangrijk is om slechts één instantie van een bepaalde klasse in een applicatie actief te hebben.
Dat verschillende redenen hebben. Zo kan de instantiatie van zo’n klasse kostbaar zijn in termen van tijd en processorkracht. Daarnaast is het soms van belang om toegang tot een bepaalde bron, bijvoorbeeld een database, te laten verlopen via één specifiek object, bijvoorbeeld om concurrency problemen te voorkomen. De meeste ASP.NET 2.0 providers implementeren wel het Singleton pattern maar beschikken niet over specifieke lock-mechanismen om gelijktijdige toegang tot bronnen te voorkomen. Dat komt omdat bij de aanroep van methoden er maar weinig gedeelde gegevens zijn. En als er al gegevens gedeeld worden, is dat alleen om te lezen.
Façade
Het laatste pattern dat in het Provider model wordt gebruikt is de Façade. Deze ontwerp-techniek is er voor bedoeld om ontwikkelaars gemak te bieden bij het aanroepen van functionaliteit van een bepaalde feature. Je zou het de ‘grootste gemene deler’ API kunnen noemen die enkel die methoden aanbiedt die het meest voor de hand liggen en de uitgebreidere, complexe methoden afschermt. Zo is de statische klasse Membership een typisch voorbeeld van zo’n Façade. Er zitten vier overloaded methoden in voor ‘CreateUser’ terwijl de MembershipProvider er maar één kent. Wanneer de ontwikkelaar er één kiest uit Membership, zorgt deze klasse voor het invullen van de eventueel ontbrekende waarden.
Provider klassen
De belangrijkste provider klassen bevinden zich in de System.Configuration.Provider namespace. Hieruit zou je kunnen afleiden dat het Provider model niet enkel voor ASP.NET 2.0 toepasbaar is, en dat is ook zo.
Alle providers erven van ProviderBase, een klasse met een tamelijke summiere definitie: een Name en Description eigenschap en één methode, Initialize. ProviderBase is een abstracte klasse omdat, gezien de beperkte functionaliteit, het direct gebruiken van deze klasse weinig zinvol is. Het belangrijkste element van ProviderBase is de Initialize-methode. Deze methode wordt door de statische feature klassen (zoals Membership) aangeroepen via de ProvidersHelper klasse en zorgt er voor dat de provider klasse geheel up en running is, en tevens maar één keer kan worden geïnstantieerd.
De Initalize methode heeft twee parameters. Name verwijst naar het name attribuut in de configuratie en config is een NameValueCollection die gevuld is met waarden uit het <add /> element behorend bij de specifieke provider definitie.
Een beetje historie
Het ASP.NET team is verantwoordelijk voor het Provider model. Er is met dit team een discussie geweest over de relatief beperkte set aan parameter-mogelijkheden voor een provider. De parameters die een provider nodig heeft kunnen immers enkel bestaan uit naam/waarde paren. Waarom is er geen gebruik gemaakt van een XmlNode of een XPathNavigator? En waarom is een geen gebruik gemaakt van de nieuwe Generics mogelijkheden van het 2.0 Framework.
De reden om geen xml boomstructuur te gebruiken is niet verklaard. Dat neemt niet weg dat het referen naar een dergelijke structuur in een naam/waarde collectie prima mogelijk is.
Het niet gebruiken van Generics heeft vooral te maken met het moment waarop het Provider model gestalte kreeg in .NET 2.0. Dat was vrij vroeg en gebaseerd op de mogelijkheden van .NET 1.1. De opzet van geconfigureerde Providers komt ook al terug in de Enterprise Library en ook de open source portal DotNetNuke. Het idee van Providers is dus allerminst nieuw of revolutionair te noemen. Daar komt bij dat Generics in eerste instantie geen onderdeel waren van de Common Language Specification (CLS) en de Microsoft regel is dat alle features van het .NET Framework zelf CLS compliant moeten zijn. Nadat het Provider model al bijna helemaal klaar was is Generics alsnog toegetreden tot de CLS.
Het is dus goed mogelijk dat in toekomstige versies nieuwe mogelijkheden van het Provider model worden toegevoegd aan de hand van Generics en uitgebreide configuratie-opties.
ProvidersHelper
De Providers klassen zitten, zoals eerder gezegd, in de System.Configuration.Providers namespace. Wie deze namespace bekijkt, ziet eigenlijk maar drie klassen:
- ProviderBase
- ProviderCollection
- ProviderException
Een eigen namespace is misschien te veel eer. Bovendien zitten andere relevante klassen in een andere namespace. De meeste Providers die standaard in .NET 2.0 geïmplementeerd zijn, hebben betrekking op ASP.NET. Vandaar dat er een ProvidersHelper klass in System.Web.Configuration zit. Dat neemt overigens niet weg dat wanneer je een eigen provider maakt die niets met ASP.NET te maken heeft je alsnog naar deze namespace kunt refereren.
De ProvidersHelper klasse is enkel bedoeld om, in de traditie van de Factory method, instanties te creëren van de gewenste Provider.
ProviderSettings
De ProviderSettings klasse vertegenwoordigt een <add /> element in de configuratie voor een specifieke Provider. Dit element ziet er bijvoorbeeld zo uit:
<add name="DefaultQuoteInformationProvider"
type="YahooQuoteInformation.YahooQuoteInformationProvider, YahooQuoteInformation"
description="Get the quotes from Yahoo."
datetimeFormat="dd-MM-yyyy hh:mm"
/>
De Name en Type eigenschappen uit de ProviderSettings klasse refereren dus naar deze name en type attributen. De overige attributen worden geladen in een NameValueCollection onder de eigenschap ‘Parameters’.
Een eigen Provider
We hebben in vogelvlucht de belangrijkste elementen van het Provider model gezien. In het overzicht aan het eind van dit artikel wordt verwezen naar meer bronnen van informatie die buiten de scope van dit artikel vallen. Het doel van het volgende voorbeeld is te leren welke elementen nodig zijn om een Provider gebaseerde feature te implementeren en te gebruiken. Het gaat dus niet zo ver als een volledige eigen implementatie van bijvoorbeeld de Membership of RoleProvider features van ASP.NET.
We maken gebruik van de onderstaande structuur. Een link naar de te downloaden bron vindt je onderaan dit artikel.
De voorbeeld feature die we willen bouwen is de mogelijkheid om prijsinformatie van aandelen op te halen. Hier zijn verschillende bronnen voor mogelijk (websites, webservices, databases). We willen gebruikers van onze ‘clientapplicatie’ in staat stellen om te switchen van de ene aanbieder van prijsinformatie naar de ander, zonder dat deze clientapplicatie harde verwijzingen heeft naar deze aanbieder. Een uitgelezen kans om deze feature te baseren op een Provider.
De basis definitie van de Provider is als volgt:
using System;
using System.Configuration.Provider;
namespace QuoteFeature
{
public abstract class QuoteInformationProvider : ProviderBase
{
public abstract string DateTimeFormat { get; }
public abstract QuoteInfo GetQuote(string symbol);
}
}
Het doel van de provider is het kunnen ophalen van een prijs van een bepaald aandeel dat herkenbaar is via zijn symbol. Een symbol is de afkorting van het aandeel zoals deze gepubliceerd worden, zoals MSFT, GOOG, ABN. Je kunt op http://finance.yahoo.com/lookup symbols opzoeken. De prijsinformatie wordt vastgelegd in een object van het type QuoteInfo. Deze klasse kent properties als: Last, UpdateDateTime, Company, en Symbol.
Het opnemen van een DateTimeFormat eigenschap is wellicht niet zo zinvol. Meestal wil je op het allerlaatste moment het besluit nemen voor het datum-tijd formaat waarin een tijdstip moet worden weergegeven. Het opnemen van deze eigenschap in een Provider is enkel omwille van het voorbeeld gedaan.
Het is mogelijk om meerdere providers te kiezen. Het kan zijn dat we dezelfde provider met andere parameters willen gebruiken of een geheel andere provider. Dit wordt mogelijk gemaakt door een eigen ProviderCollection.
public class QuoteInformationProviderCollection : ProviderCollection
{
public override void Add(ProviderBase provider)
{
if (provider == null)
throw new ArgumentNullException("You need to supply a " +
"provider reference.");
if (!(provider is QuoteInformationProvider))
throw new ArgumentException("The supplied provider needs to " +
"derive from QuoteInformationProvider.");
base.Add(provider);
}
new public QuoteInformationProvider this[string name]
{
get { return (QuoteInformationProvider)base[name]; }
}
public void CopyTo(QuoteInformationProvider[] array, int index)
{
base.CopyTo(array, index);
}
}
Je ziet dat het maken van een eigen ProvidersCollection voornamelijk generieke code is. De indexer (this[]) en de CopyTo methode geven simpelweg de gerefereerde instantie van de Provider terug.
De feature-klasse zorgt er voor dat de ontwikkelaar geen specifieke kennis van de Provider nodig heeft om de gewenste functionaliteit aan de gebruiker te leveren. De API moet daarvoor natuurlijk wel voldoende functionaliteit bevatten. Zoals eerder is aangegeven, is de feature-klasse een voorbeeld van het Façade design pattern.
public static class QuoteInformation
{
public static QuoteInformationProvider defaultProvider;
public static QuoteInformationProviderCollection providerCollection;
public static QuoteInformationProvider Provider
{
get { return defaultProvider; }
}
public static QuoteInformationProviderCollection Providers
{
get { return providerCollection; }
}
public static QuoteInfo GetQuote(string symbol)
{
return Provider.GetQuote(symbol);
}
…
De default provider is aan te spreken via de Provider eigenschap. Als je meerdere Providers geconfigureerd hebt, kan je de specifieke provider benaderen via de Providers eigenschap. De feature-klasse biedt in hoofdzaak maar één relevante functie, namelijk het ophalen van Quote informatie. Je kunt je voorstellen dat features als Membership, Site Navigation, Health Monitoring een hele reeks van statische methoden kennen.
Een feature gebaseerd op een Provider kent drie levensfasen:
- Eerst is de feature niet geïnitialiseerd. Iedere aanroep van een methode uit de feature slingert de initialisatie aan.
- Als de initialisatie gelukt is, zijn de feature-methoden direct aanroepbaar; de feature is zogezegd geïnitialiseerd.
- Als de initialisatie mislukt kan de feature nog steeds geïnitialiseerd zijn, maar zich in een fout-status bevinden. De reden van de mislukte initialisatie moet wel ergens worden opgeslagen.
Het gevolg van deze fasen is dat we ofwel een volledig werkende statische klasse tot onze beschikking hebben, ofwel informatie over het mislukken van de initialisatie. In het voorbeeld zetten we een eventuele fout, in de vorm van een exceptie, in een eigenschap van de feature-klasse. Door deze eigenschap te onderzoeken kunnen we erachter komen waarom de initialisatie niet lukte. We voorkomen op deze manier ook dat de hele initialisatie telkens opnieuw wordt geprobeerd zodra in de applicatie een feature-methode wordt aangeroepen.
// initialization related variables and logic
private static bool isInitialized = false;
private static Exception initializationException;
private static object inializationLock = new object();
static QuoteInformation()
{
Initialize();
}
De feature-klasse houdt haar status vast in de twee private variabelen: isInitialized en initializationException. De volledige implementatievan de initialisatie methode staat hieronder. Deze code is opnieuw tamelijk generiek van opzet. Wanneer je boeken of artikelen over het Provider model leest zal je de onderstaande code geregeld terugzien.
private static void Initialize()
{
if (isInitialized) {
if (initializationException == null) {
return;
}
else {
throw initializationException;
}
}
// start initialization
lock (inializationLock) {
if (isInitialized) {
if (initializationException == null) {
return;
}
else {
throw initializationException;
}
}
try {
QuoteInformationConfigurationSection cs =
(QuoteInformationConfigurationSection)
ConfigurationManager.GetSection("quoteInformation");
if (cs.DefaultProvider == null || cs.Providers == null ||
cs.Providers.Count == 0)
throw new ProviderException("The feature needs a default provider " +
"and at least one provider definition.");
// instantiate the feature's provider
providerCollection = new QuoteInformationProviderCollection();
ProvidersHelper.InstantiateProviders(
cs.Providers,
providerCollection,
typeof(QuoteInformationProvider));
providerCollection.SetReadOnly();
defaultProvider = providerCollection[cs.DefaultProvider];
if (defaultProvider == null) {
throw new ConfigurationErrorsException(
"The default feature provider was not specified.",
cs.ElementInformation.Properties["defaultProvider"].Source,
cs.ElementInformation.Properties["defaultProvider"].LineNumber);
}
}
catch (Exception ex) {
initializationException = ex;
isInitialized = true;
throw ex;
}
isInitialized = true;
}
}