Entity Framework en AutoMapper, hoe zorg je dat jouw objecten dynamisch zijn?

Ingediend door Dirk Hornstra op 07-feb-2022 08:25

Twee van mijn collega's bij TRES zijn bezig met het opbouwen van een structuur waar we een aantal sites op willen laten draaien. Je hebt dan een standaard indeling van een bepaalde tabel, met bepaalde velden. Maar (natuurlijk) heb je ook omgevingen waar er extra velden zijn, de boel "net iets anders" ingericht is.

Ik heb hierbij wat advies gegeven, maar dat was alleen op basis van theoretische kennis. Ik weet dat je met interfaces kunt werken, abstracte classes kunt gebruiken, bij een aanroep van een functie het type mee kunt geven: public List<T> GetProduct<T>() where T: class, new(). En daar moet je een heel eind mee komen. Maar goed, ook ik weet, soms kan het redelijk simpel klinken, als je het moet bouwen loop je vaak tegen problemen aan en moet je maar zien of je dat kunt oplossen.

Ik weet zo niet wat de status van het project is, wel weet ik dat ik met een eigen structuur voor mijn prijs-bewust.nl website aan de slag ga. En omdat ik toch niet helemaal fit ben ga ik kijken of ik een werkende structuur op kan zetten. Mogelijk hebben mijn collega's er wat aan en scheelt het ze de problemen die ik nu al tegen kom.

Zo was mijn eerste insteek om een IProduct te maken, die in het entity-model te gebruiken met DbSet<IProduct> en zo de boel met AutoMapper om te zetten naar een Product-class. Voor de mensen die AutoMapper niet kennen, je hebt een database-tabel Product met veld ProductId en veld Name, die class heb je in je Data-project ook en je hebt in je Domain-project een vergelijkbare class waarbij je zaken net even anders noemt. Met een AutoMapper kun je jouw database-objecten "geruisloos" omzetten naar een POCO (plain old C-sharp object), waarmee je in je code verder kunt werken.

Nou, geen interface gebruiken dus. Voor het "mappen" heeft AutoMapper een concrete class nodig, een interface gaat niet werken. 

Ik had nog het "slimme" idee om de class Product "partial" te maken. Zo kun je een class over meerdere .cs-bestanden verdelen. Handig om een Product "uit te breiden". Maar ook hier: niet doen, want die class zit in zijn eigen assembly (DLL) en als je in jouw project dezelfde namespace gebruikt en hier de partial Product gebruikt krijg je al de warning dat er al een class is in een andere DLL en dat die gebruikt wordt. Partial classes zijn bedoeld om in hetzelfde project te gebruiken.

Dus ja, het heeft me een zaterdag en zondag gekost. Maar het is wel gelukt!

De eerste tip is dat je een veld "Discriminator" toe moet voegen. Die naam zou op zich anders kunnen, maar omdat ik tussentijds tegen wat problemen aanliep heb ik dit zo gelaten. Want als je een "eigen" Product-class hebt, die erft van de Product-class in de base (en die heb je wat extra eigenschappen gegeven), dan gaat AutoMapper zeuren: ik zie nu 2 Product-classes en kan het verschil niet zien. Dus in de Product-class in de base is dit een boolean-veld met waarde false, als jouw implementatie een uitbreiding heeft op de Product-class, die geef je dan een waarde true in dit veld. Op zich is dit ook wel mooi, want zou je op een later tijdstip willen controleren of er omgevingen zijn met aangepaste product-types, dan hoef je alleen maar hier te checken op de waarde "true".

Ik heb het project op Github gezet, zodat je het kunt clonen en kunt bekijken. In mijn geval heb ik een lokale mySQL-database gebruikt. We lopen de projecten even door:

ProductPortal.Data

Dit is het basis-project voor de koppeling met de database. Je ziet hier een abstracte class BaseDatabaseContext, deze wordt geimplementeerd in de DatabaseContext en gekoppeld in de Domain-projecten. Je ziet dat producten en categorieen opgevraagd worden met het Type product/categorie. 

Bij de Models zie je Product en Category, dat zijn de basis-implementaties voor deze classes. Je ziet hier ook een "DummyProduct" , dat lichten we straks toe bij de DatabaseContext (maar de naam is op zich wel "self explaining").

Vervolgens heb je DatabaseContext, deze koppelt naar de fysieke database. Als je een eigen product maakt (dus uitbreiding op je Product-class), dan moet je ook een eigen class maken die erft van DatabaseContext. Maar daar zitten een paar haken en ogen aan. Want als je een eigen Product-class hebt, dan is het Discriminator-veld gevuld met "true". Maar als het basis Product-class object goed genoeg voor je is, dan is dat Discriminator-veld altijd gevuld met "false". En AutoMapper wil dan ook een object gekoppeld hebben aan de "true" variant: daar is dus het DummyProduct-object voor. Maar als je een eigen implementatie hebt, dan moet dat Product-object aan true gekoppeld worden (anders krijg je de foutmelding dat DummyProject daar al aan vast zit). Daarom de useDummyDiscriminators-parameter.

ProductPortal.Domain

In dit basis-project zitten de instellingen hoe je in code met je objecten werkt. 

In Interfaces zit een IProduct, waarin je de structuur van een standaard Product ziet. 

In Models heb je een Product-class, het object wat je in code gebruikt. Ook een Category-class, het object wat je ook gebruikt.

De normale aanroep kun je dus zo uitvoeren:


            var dataHandler = new DataHandler(_connectionstring);
            var products = dataHandler.GetProducts();

Maar goed, de eigen implementatie.

ProductPortal.Implementation.Data

In de map Models heb je een eigen Product-class, deze erft van de Product-class van de basis. 

De class DatabaseContext erft van de basis-DatabaseContext, bij OnModelCreating koppelen we de discriminator als het veld.  De eigen _products-variabele is een implementatie van de eigen Product-class. De Products<T>-functie retourneert hier de eigen Product-class.

ProductPortal.Implementation.Domain

In de map Models heb je jouw eigen product-class, deze erft van de Product-class van de basis, dus je ziet alleen de extra velden.

De DataHandler erft van de basis-DataHandler. Het enige wat je hier hoeft te doen is de constructor over-rulen en jouw eigen Product-class aan de AutoMapper koppelen. 

 

Conclusie

Als je het werkend hebt lijkt het niet zo moeilijk, maar als je het bouwt raak je wel eens in de war met alle foutmeldingen. Dus ik hoop dat een ieder met een vergelijkbaar project op basis van dit voorbeeld zijn/haar eigen project werkend kan krijgen!

Het project wat ik hiervoor opgezet heb, heb ik online gezet als een Github-repo en kun je hier bekijken: link.