Ik ben met .NET aan het worstelen om op een elegante manier data validatie te doen, maar ik heb sterk het gevoel dat ik het wiel opnieuw aan het uitvinden ben. Iemand moet vast dit probleem kenne, maar ik heb nog nergens een mooie oplossing gezien.
Het volgende is het geval. Ik ben een ASP.NET applicatie aan het bouwen op basis van het Enterprise Template "Simple Distributed Application" met wat kleine aanpassingen hier en daar. Het is een solution met een losse projecten voor de WebUI, BusinessRules, DataAccess, en een Common project met daarin typed datasets. De lagen communiceren ook in de bovengenoemde volgorde met elkaar, waarbij typed datasets worden doorgegeven. Dit werkt allemaal prima.
Het probleem waar ik tegenaan loop is dat ik validatie wil doen van de datasets voordat ik ze opsla. Natuurlijk heb ik basisvalidatie in mijn WebUI (lengte, type, niet null) en in mijn dataset (idem). Daarnaast kan ik in mijn business rules nog een paar dingen checken (bijv. bij een order: is het ordertotaal boven de 7000, dan moet er een verzekering worden afgesloten voor het verzenden), maar daarna wordt het allemaal een beetje tricky, zelfs in de meest eenvoudige gevallen. Als voorbeeld neem ik een hele simpele dataset met 1 tabel Divisie, met daarin 2 kolommen: DivisieID (unique, autogenerated) en DivisieNaam (unique).
Op het ogenblik dat ik een nieuwe divisie wil aanmaken in het edit scherm, gebeurt het volgende:
DivisieDetail:
BusinessRules: (simpele entiteit, dus weinig spannends hier)
DataAccess:
Op het ogenblik dat je een niet unieke divisienaam invoert, krijg je dus een SqlExceptie. Ik stop nu de error messege van de SqlError in de RowError van de eerste (en in dit geval enige) datarow, zodat in de WebUI met HasErrors de error gevonden en afgedrukt kan worden. Uiteindelijk moet er natuurlijk een elegantere boodschap komen, maar dat is een latere zorg.
Het probleem is dat dit niet werkt in een dataset met meer dan 1 record, want er is mijns inziens geen enkele wijze om te weten komen bij welke datarow binnen de dataset de fout optrad, waardoor je niet de RowError property van de juiste datarow kunt zetten, wat wel van belang is.
Ik heb zelf de volgende oplossingen / work arronds bedacht, maar geen van allen vind ik ze elegant en ze hebben allemaal nadelen:
1) Zet de .ContinueUpdateOnError property van de data adepter uit
In dit geval wordt er geen exceptie gegooid en worden de RowErrors van de problematische datarows gezet. Nadeel is dat je met complexe datasets (bijv. met relaties) veel te weinig grip hebt op het eventueel zinvol afhandelen van fouten.
Bovendien zijn de RowErrors nu gevuld met de SqlError .Messege, die uiteindelijk niet voldoet voor consumptie van eindgebruikers. Bij mijn huidige constructie kan ik uit de SqlError nog een error number halen en dat gebruiken om een zinvolle, mooie foutmelding te genereren, maar op deze manier zijn de SqlErrors noit bereikbaar. Het gaan parsen van de messages in .RowError en dat vertalen kan maar is ook niet echt elegant (voorzichtig uitgedrukt).
2) Roep de .Update van de data adepter per gewijzigde DataRow aan
Bij datasets waarin veel gewijzigd is betekent dit een aardige belasting op de data adepter. Daarnaast moet je de datarows weer mergen met je oorspronkelijke dataset, wat volgens mij ook een aardige belasting gaat geven.
3) Voorkom dergelijke SqlExceptions door alle datavalidatie te doen voordat je de de dataset probeert op te slaan. Dat kan op 2 manieren:
a) Gebruik de dataset validatie:
Je kan binnen de dataset constraints opgeven zoals UniqueConstraint en ForeignKeyConstraint. Dit werk prima zolang ALLE data in de dataset zit. Dwz: om de UniqueConstraint zinvol te kunnen testen zul je de hele tabel in de dataset moeten opnemen. Om de ForeignKeyConstraint te kunnen testen moet je alle gerelateerde tabellen en hun data opnemen in de dataset.
Het overduidelijke nadeel hiervan is dat je gigantisch veel data gaat ophalen.
b) Schrijf voor iedere constraint een losse check:
Je zou voor een UniqueConstraint een query kunnen runnen die een select doen die zoekt naar rows met de nieuw te inserten waarde, en als er meer dan 0 records worden gevonden, de RowError zet. Je kunt voor een ForeignKeyConstraint een soortgelijke quiery kunnen runnen op de gerelateerde tabel om te kijken of de ID van het huidige records daar bestaat en aan de hand daarvan de RowError zetten. Dat betekent voor complexe tabellen ontzettend veel checks schrijven en veel losse queries.
Wow ....
... dat je dit gezwam helemaal hebt gelezen. Maar ... misschien weet jij wel dingen die ik niet weet of heb je enorm verhelderende ideeen. Let me know what you think. Ik ben geneigd om oplossing 2 te kiezen, maar vraag me enorm af of dat wel de goeie manier is ...
Het volgende is het geval. Ik ben een ASP.NET applicatie aan het bouwen op basis van het Enterprise Template "Simple Distributed Application" met wat kleine aanpassingen hier en daar. Het is een solution met een losse projecten voor de WebUI, BusinessRules, DataAccess, en een Common project met daarin typed datasets. De lagen communiceren ook in de bovengenoemde volgorde met elkaar, waarbij typed datasets worden doorgegeven. Dit werkt allemaal prima.
Het probleem waar ik tegenaan loop is dat ik validatie wil doen van de datasets voordat ik ze opsla. Natuurlijk heb ik basisvalidatie in mijn WebUI (lengte, type, niet null) en in mijn dataset (idem). Daarnaast kan ik in mijn business rules nog een paar dingen checken (bijv. bij een order: is het ordertotaal boven de 7000, dan moet er een verzekering worden afgesloten voor het verzenden), maar daarna wordt het allemaal een beetje tricky, zelfs in de meest eenvoudige gevallen. Als voorbeeld neem ik een hele simpele dataset met 1 tabel Divisie, met daarin 2 kolommen: DivisieID (unique, autogenerated) en DivisieNaam (unique).
Op het ogenblik dat ik een nieuwe divisie wil aanmaken in het edit scherm, gebeurt het volgende:
DivisieDetail:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public void SaveNew() { Common.DivisieDS.DivisieRow newDivisie = this.divisieDS.Divisie.NewDivisieRow(); newDivisie["DivisieNaam"] = this.txtDivisieNaam.Text; this.divisieDS.Divisie.AddDivisieRow(newDivisie); BusinessRules.Divisie myDivisie = new BusinessRules.Divisie(); this.divisieDS = myDivisie.Save(this.divisieDS); if (this.divisieDS.HasErrors) { this.lblStatus.Text = "ERROR: "+ this.divisieDS.Divisie[0].RowError; } else { Response.Redirect("DivisieDetail.aspx?DivisieID=" + newDivisie.DivisieID.ToString()); } } |
BusinessRules: (simpele entiteit, dus weinig spannends hier)
C#:
1
2
3
4
5
6
| public Common.DivisieDS Save(Common.DivisieDS divisieDS) { DataAccess.Divisie divisie = new DataAccess.Divisie(); divisieDS = divisie.Save(divisieDS); return divisieDS; } |
DataAccess:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public Common.DivisieDS Save(Common.DivisieDS divisieDS) { try { divisieDA.Update(divisieDS); } catch (SqlException sqlEx) { foreach (SqlError sqlErr in sqlEx.Errors) { divisieDS.Divisie.Rows[0].RowError = divisieDS.Divisie.Rows[0].RowError + "; " + sqlErr.Message; } } return divisieDS; } |
Op het ogenblik dat je een niet unieke divisienaam invoert, krijg je dus een SqlExceptie. Ik stop nu de error messege van de SqlError in de RowError van de eerste (en in dit geval enige) datarow, zodat in de WebUI met HasErrors de error gevonden en afgedrukt kan worden. Uiteindelijk moet er natuurlijk een elegantere boodschap komen, maar dat is een latere zorg.
Het probleem is dat dit niet werkt in een dataset met meer dan 1 record, want er is mijns inziens geen enkele wijze om te weten komen bij welke datarow binnen de dataset de fout optrad, waardoor je niet de RowError property van de juiste datarow kunt zetten, wat wel van belang is.
Ik heb zelf de volgende oplossingen / work arronds bedacht, maar geen van allen vind ik ze elegant en ze hebben allemaal nadelen:
1) Zet de .ContinueUpdateOnError property van de data adepter uit
In dit geval wordt er geen exceptie gegooid en worden de RowErrors van de problematische datarows gezet. Nadeel is dat je met complexe datasets (bijv. met relaties) veel te weinig grip hebt op het eventueel zinvol afhandelen van fouten.
Bovendien zijn de RowErrors nu gevuld met de SqlError .Messege, die uiteindelijk niet voldoet voor consumptie van eindgebruikers. Bij mijn huidige constructie kan ik uit de SqlError nog een error number halen en dat gebruiken om een zinvolle, mooie foutmelding te genereren, maar op deze manier zijn de SqlErrors noit bereikbaar. Het gaan parsen van de messages in .RowError en dat vertalen kan maar is ook niet echt elegant (voorzichtig uitgedrukt).
2) Roep de .Update van de data adepter per gewijzigde DataRow aan
Bij datasets waarin veel gewijzigd is betekent dit een aardige belasting op de data adepter. Daarnaast moet je de datarows weer mergen met je oorspronkelijke dataset, wat volgens mij ook een aardige belasting gaat geven.
3) Voorkom dergelijke SqlExceptions door alle datavalidatie te doen voordat je de de dataset probeert op te slaan. Dat kan op 2 manieren:
a) Gebruik de dataset validatie:
Je kan binnen de dataset constraints opgeven zoals UniqueConstraint en ForeignKeyConstraint. Dit werk prima zolang ALLE data in de dataset zit. Dwz: om de UniqueConstraint zinvol te kunnen testen zul je de hele tabel in de dataset moeten opnemen. Om de ForeignKeyConstraint te kunnen testen moet je alle gerelateerde tabellen en hun data opnemen in de dataset.
Het overduidelijke nadeel hiervan is dat je gigantisch veel data gaat ophalen.
b) Schrijf voor iedere constraint een losse check:
Je zou voor een UniqueConstraint een query kunnen runnen die een select doen die zoekt naar rows met de nieuw te inserten waarde, en als er meer dan 0 records worden gevonden, de RowError zet. Je kunt voor een ForeignKeyConstraint een soortgelijke quiery kunnen runnen op de gerelateerde tabel om te kijken of de ID van het huidige records daar bestaat en aan de hand daarvan de RowError zetten. Dat betekent voor complexe tabellen ontzettend veel checks schrijven en veel losse queries.
Wow ....
[ Voor 4% gewijzigd door Verwijderd op 18-07-2005 14:31 ]