Toon posts:

[.NET] Data validatie architectuur

Pagina: 1
Acties:

Verwijderd

Topicstarter
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:
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 .... _/-\o_ ... 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 ...

[ Voor 4% gewijzigd door Verwijderd op 18-07-2005 14:31 ]


  • joopst
  • Registratie: Maart 2005
  • Laatst online: 01-10-2024
volgens mij kan je het beste niet leunen op excepties uit de database voor de werking van je app.

als er een exception komt, dan moet is dat echt een uitzondering.

dus als je iets naar de database stuurt, dan zou het normaliter altijd moeten lukken.

Voor je validatie, volgens mij is ook nog wel een optie #4. Namelijk geen gebruik te maken van datasets, maar van eigen classes.

success!

Verwijderd

Mijn voorkeur gaat uit naar het uitvoeren van de validatie in de DataAcces laag als het gaat om regels zoals "Bestaat de naam van de Divisie al?".

Je zou het ook m.b.v. stored procedures kunnen regelen als de snelheid in de DataAccess laag niet toereikend is.

Als je 1 groot datablok in 1 keer wilt opslaan, dan kan je ook het opslaan in 1 grote stored procedure regelen, waarbinnen ook nog eens alle checks worden uitgevoerd. Het voordeel hiervan is dat je met 1 aanroep klaar bent. Das ook wel lekker als je een keer handmatig een stored procedure wil gaan aanroepen.

Verwijderd

Topicstarter
@joopst: Het niet gebruiken van datasets is geen optie, aangezien alles binnen de applicatie leunt op typed datasets. Daarnaast is het bouwen van classes met vergelijkbare functionaliteit natuurlijk behoorlijk veel extra werk.

@IntroV: Het liefst zou ik ook niet met exceptions werken en alles voordat het naar de database gaat gevalideerd wordt. Maar dan kom ik dus op oplossing 3a of 3b. Zie jij dat ook zo, of heb je andere ideeen?

Ik ben niet een enorme voorstander van heel zwaar gebruik van stored procedures, omdat je dan functionaliteit gaat verspreiden over verschillende deelsystemen op een niet logische, homogene manier (tenminste, niet in .Net 1.1 / SQL Server 2000, hoewel ik heb begrepen dat het in .Net 2.0 / SQL Server 2005 beter is). Als het gaat om op een paar plaatsen behoorlijke performance winst te krijgen, dan ben ik ervoor. Voor relatief simpele CRUD (Create, Read, Update, Delete) functionaliteit ook geen probleem.

Maar als je voor iedere constraint een stored procedure zou moeten bouwen of iedere CRUD stored procedure moet volproppen met validatiecode en om die weer door te geven en in de data access laag af te vangen, dan zit je volgens mij met een enorm onderhoudprobleem en elegant is het ook niet.

Beiden bedankt voor de antwoorden, maar ik hoop nog steeds op iets mooiers ... misschien ijdele hoop. :/

Verwijderd

Tja, ik denk dat je toch uitkomt op alles in de Datalaag regelen.

En dan krijg je code zoals:

C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public sub SaveDivision
{
   bool Result = CheckUniqueDivision(DivisionName);

   if ( Result )
   {
      Result = CheckCreditCardNr(CC);
   }

   if ( Result )
   {
      Result = CheckCorrectAddress(Address);
   }

   //Alles gelukt, sla data op
   if ( Result )
   {
      ExecuteSQLStatement();
   }

}



Zo lastig is dit toch niet?

Verwijderd

Topicstarter
Idd, ik denk dat ik het in de datalaag zal gaan stoppen. Nee, niet lastig, ik dacht ook aan zoiets, en dan datareaders gebruiken in de CheckXxxxx, maar goed, ik hoopte nog steeds op een geweldig mooi, eenvoudig, light weight mechanisme waar weinig code voor nodig zou zijn, maar ik vrees dat dat er gewoon niet in zit.

Thanx. :)
Pagina: 1