ASP.NET Providers: Weer een Desillusie
Software ontwikkelaars zijn een beetje naïef. Elke keer wanneer zich nieuwe technologieën aandienen zijn ze razend enthousiast. Maar wanneer ze enige tijd gewerkt hebben met die vernieuwende technieken volgt de desillusie, steevast gevolgd door hernieuwd enthousiasme voor andere technieken die nog nieuwer en dus nog “mooier” zijn dan de voorgaande. Dit lijkt een cyclus te zijn die nooit doorbroken wordt.
Dit viel mij op toen ik mij laatst voor de zoveelste keer heb geërgerd aan de standaard ASP.NET providers voor roles, profile en membership. Deze volgen het provider pattern dat door Microsoft geïntroduceerd is in ASP.NET 2.0 en dat door jan-en-alleman bejubeld is als het fraaiste stukje architectuur sinds de Erasmusbrug. Welnu, het provider pattern zal in theorie ongetwijfeld mooi zijn, maar met betrekking tot de praktische uitwerking in ASP.NET bevind ik mij in het laatste deel van de cyclus: de desillusie. Ik zal elk van de drie bekende providers van ASP.NET behandelen en uitleggen waarom ik inmiddels reikhalzend uitkijk naar het volgende nieuwe kunstwerkje.
Software ontwikkelaars zijn een beetje naïef
MembershipProvider
De MembershipProvider is bedoeld om een eigen implementatie te kunnen maken voor de afhandeling van het registreren, inloggen en uitloggen van gebruikers op een ASP.NET website. Als je je netjes houdt aan de voorgeschreven interface, dan kunnen diverse standaard user interface controls het hele authenticatie-circus voor hun rekening nemen. Maar dan moet je wel netjes de voorgeschreven interface volgen, en daar begint de ellende. Ik zal een voorbeeld geven.
Public Overrides Function CreateUser(_
ByVal username As String, _
ByVal password As String, _
ByVal email As String, _
ByVal passwordQuestion As String, _
ByVal passwordAnswer As String, _
ByVal isApproved As Boolean, _
ByVal providerUserKey As Object, _
ByRef status As _
System.Web.Security.MembershipCreateStatus) As _
System.Web.Security.MembershipUser
Throw New NotImplementedException
End Function
Listing 1: CreateUser functie van MembershipProvider
De CreateUser functie moet geïmplementeerd worden ten behoeve van de registratie van nieuwe gebruikers. Hierbij vraag ik mij af waarom een password question en answer hier standaard bij inbegrepen zitten. Veel applicaties gebruiken een dergelijk mechanisme namelijk helemaal niet. Maar goed, teveel informatie is meestal niet het probleem. Ik heb daarentegen wel behoefte aan een verplichte volledige naam van de user, zijn voorkeursinstellingen, en het ID van de persoon die deze user probeert toe te voegen aan de database. Goed, dat is natuurlijk allemaal oplosbaar door die verplichte gegevens binnen deze functie zelf aan te vullen voordat alles opgeslagen wordt in de database. Ik begrijp dat er bij het definiëren van interfaces keuzes gemaakt moeten worden, al had ik in dit geval dan gehoopt op meerdere overloads ter ondersteuning van verschillende situaties. En een extra IUserData interface had ook die flexibiliteit kunnen bieden. Maar het wordt vervelender.
Public Overrides Function ValidateUser( _
ByVal username As String, _
ByVal password As String) As Boolean
'my implementation...
End Function
Listing 2: ValidateUser functie van MembershipProvider
Wanneer een gebruiker probeert in te loggen, moet de ValidateUser functie gebruikt worden om te controleren of dat wel mag. De functie geeft een boolean waarde terug en de user interface kan dus wanneer nodig een nette “access denied” melding geven. Hoezo “access denied”? Is de username onbekend in het systeem? Is het password onjuist? Is het account gedeactiveerd? Geen idee. Ja, als ontwikkelaar van deze functie weet ik dat wel, maar ik kan het niet aan de client teruggeven. Mogelijk heeft iemand besloten dat je dergelijke details om veiligheidsredenen niet aan een gebruiker behoort te tonen, maar daar heb ik om redenen van gebruiksvriendelijkheid toch grote moeite mee. Ik zou die overweging als bouwer van de client dan ook graag zelf willen maken. Het teruggeven van (bijvoorbeeld) een IValidationResult interface had hier uitkomst kunnen bieden. Maar helaas, om iets anders dan een Boolean terug te geven moet ik nu om de strikte interface heen werken zodat de client de extra informatie via een omweg kan ophalen en weergeven aan de gebruiker.
Al sinds de tijd dat de Hellenisten zich toegang probeerden te verschaffen tot Troje is dit patroon in gebruik
RoleProvider
De standaard RoleProvider heeft ook zo zijn eigenaardigheden. Deze provider behandelt de autorisatie van de gebruikers, dus wat mogen ze wel en niet binnen het systeem? Velen van ons zijn bekend met het traditionele patroon van autorisatiemechanismen, bestaande uit gebruikers, rollen en permissies. Gebruikers kunnen meerdere rollen hebben, en per rol zijn meerdere permissies beschikbaar. Al sinds de tijd dat de Hellenisten zich toegang probeerden te verschaffen tot Troje is dit patroon in gebruik … maar helaas niet bij de RoleProvider.
Public Overrides Function GetRolesForUser( _
ByVal username As String) As String()
'my implementation...
End Function
Listing 3: GetRolesForUser functie van RoleProvider
De functie GetRolesForUser die binnen de RoleProvider geïmplementeerd moet worden, levert de client (ASP.NET zelf) een verzameling rollen aan de hand van een gebruikersnaam. Maar als ontwikkelaar ben ik helemaal niet geïnteresseerd in de rollen van een gebruiker. Ik wil van een gebruiker namelijk alleen zijn permissies weten. Ik wil in mijn applicatie niet testen of iemand lid is van de rol “Finance”. Ik wil testen of iemand de permissie “CanCreateInvoice” heeft. Alleen op die manier kan ik ervoor zorgen dat de permissies van de rol Finance op elk moment door de applicatiebeheerder gewijzigd kunnen worden, zonder dat ik mijn applicatie hoef open te breken om rolnamen in de code aan te passen.
Listing 4: Configuratie van RoleProvider in web.config
In het web.config bestand vinden we op diverse plekken terug dat autorisatie geregeld wordt met rolnamen. Deze ben ik feitelijk aan het misbruiken als ik hier permissienamen voor in de plaats invul. En om verwarring bij mijn eigen collega’s te voorkomen heb ik de echte roles in mijn hele applicatie hernoemd naar groups. Microsoft had dit kunnen voorkomen door naast de RoleProvider ook een PermissionProvider te leveren. Nu moet ik telkens een hoop uitleggen aan elke nieuwe ontwikkelaar die mijn code probeert te lezen. (Iets dat ik nu eindelijk kan voorkomen door gewoon dit artikel onder hun neus te duwen ;-))
ProfileProvider
Als laatste komt de ProfileProvider aan de orde. In theorie geeft deze provider je de mogelijkheid om in je webapplicatie op gemakkelijke wijze toegang te krijgen tot de profielgegevens van de ingelogde gebruiker. Met een goed geïmplementeerde profile provider, en een profieldefinitie in het web.config bestand, zul je in de webapplicatie direct gebruik kunnen maken van bijvoorbeeld Profile.UserName, Profile.EmailAddress, enz... Tot zover de theorie.
type="System.Int32"/>
type="System.String"/>
type="System.String"/>
type="System.String"/>
Listing 5: Configuratie van ProfileProvider in web.config
In de praktijk loop je al snel tegen de beperking aan dat in veel applicaties niet alle gebruikers hetzelfde type profiel hebben. De beheerders, auteurs, reviewers en guests hebben in mijn applicatie slechts ten dele dezelfde profielgegevens. Om hiermee om te gaan ben ik gedwongen tot het platslaan van alle mogelijke profielvariabelen naar één generiek profiel, wat extra overhead zal opleveren. Het was fijn geweest als deze provider meerdere profieltypen had kunnen ondersteunen. Nu moet ik eigen specifieke functies bouwen per profieltype, wat de hele profile provider zo goed als overbodig zal maken. Daar komt nog bij dat de Profile class door ASP.NET automatisch gegenereerd wordt, en dus alleen beschikbaar is binnen de ASP.NET webpagina’s. Als je, net als ik, zoveel mogelijk business logica in een aparte class library stopt, dan is die Profile class daar helemaal niet toegankelijk. Je moet je dan toegang verschaffen tot dezelfde informatie via een onhandige constructie zoals HttpContext.Current.Profile.GetPropertyValue("Identity.ID"). En die is niet type-safe! Kortom, alle voordelen van deze provider zijn inmiddels allang niet meer zichtbaar in mijn achteruitkijkspiegel.
Architectuur
Een belangrijke reden voor veel ontwikkelaars om zich te wenden tot de standaard providers in ASP.NET is dat deze een betere architectuur zouden afdwingen waarmee de implementatie op een flexibele manier gescheiden wordt van de interface. Maar je bereikt juist het tegenovergestelde wanneer de gekozen opzet niet generiek genoeg is om een serieuze applicatie te kunnen ondersteunen. Ik moet me in allerlei bochten wringen om simpele dingen voor elkaar te krijgen, waarbij ik op lelijke manieren de beperkingen van de interfaces moet zien te omzeilen. Het gevolg is dat het toepassen van deze providers een negatief effect heeft op de kwaliteit van mijn code. Bovendien gaat alle tijdwinst die eventueel behaald kan worden door het toepassen van standaardisatie volledig op aan de extra tijd die ik kwijt ben aan het afhandelen, testen en documenteren van ongewenste neveneffecten. Mij resteert nog slechts het voordeel dat je de providers via een web.config zo gemakkelijk kunt vervangen. Maar is dat echt een voordeel? Ik heb in twee jaar tijd nog nooit zo’n vervanging hoeven doen. Ik had eigenlijk gewoon vanaf het begin een eigen flexibele interface en consistente implementatie kunnen opzetten. Zonder standaard provider interfaces.
Het gevolg is dat het toepassen van deze providers een negatief effect heeft op de kwaliteit van mijn code
Conclusie
Er worden een hoop voordelen toegeschreven van het toepassen van het provider pattern. Deze voordelen zijn ongetwijfeld in theorie goed haalbaar. Maar in de praktijk hangt het er vanaf of een specifieke interface definitie flexibel genoeg is om een wezenlijke bijdrage te leveren aan een nette architectuur van de beoogde applicaties. Dat betekent dat die interfaces niet alleen geschikt moeten zijn voor “hello world” webapplicaties maar ook voor serieuze toepassingen. In het geval van de drie standaard providers van ASP.NET is dat, naar mijn mening, niet het geval. Volgens enkele van mijn collega’s ben ik met deze opinie een beetje (te) negatief. Dat is natuurlijk waar. Ik hoop gewoon dat ik met dit artikel een beetje tegengas heb kunnen geven aan alle (te) positieve verhalen. Het is mijn bedoeling dat de lezer hierdoor in staat is een betere afweging te maken. Door alle positieve berichtgeving werd ik namelijk aanvankelijk heel enthousiast. Maar inmiddels kijk ik een beetje teleurgesteld uit naar nieuwere innovatieve technieken waar ik dan opnieuw (te) enthousiast over zal raken. Net als iedereen ben ik zelf namelijk ook liever naïef dan ontevreden.