Entity Framework en mySQL (mariaDB) werkt niet altijd even goed

Ingediend door Dirk Hornstra op 13-mar-2023 21:02

Al bijna 2 jaar doet mijn online Fitbit-applicatie het niet meer. De website draait nog wel, maar de gegevens worden niet meer bijgewerkt. En dat is jammer. Want ik kan op mijn Fitbit elke dag wel zien of ik het aantal stappen al gehaald heb, maar heb dus niet makkelijk inzichtelijk of ik "eigenlijk deze week" nog wat extra stappen zou moeten zetten. De wekelijkse mail van Fitbit is een soort samenvatting, dus niet exact per dag hoeveel stappen je hebt gelopen, alleen het totaal. En ik kon daar ook de totalen per jaar zien. Zo kun je dus ook zien of je op schema zit om hetzelfde aantal stappen als vorig jaar te doen. Of nog beter: meer stappen. Elk jaar wat meer om in conditie te blijven.

Omdat ik nu een nieuwe laptop heb en onder de knie begin te krijgen hoe Docker werkt, heb ik in een Docker container een dump van mijn database geïmporteerd. Hierna mijn .NET web-applicatie gestart en proberen in te loggen. Alleen, dat werkt niet. Ik krijg een foutmelding over DBNull. Wat zoektochten geven mij als resultaat dat het aan de versie van MariaDB ligt en je een oudere versie moet gebruiken. Dat zou kunnen, maar ik wil kijken of het met een recente versie van MariaDB wel werkt. Want "ga terug naar een oude versie", dat klinkt niet goed.

Let op, dat is veel meer werk! Dus als je zelf iets moet doen/fixen en je wilt er niet teveel tijd aan besteden, dan is dat waarschijnlijk toch de beste optie: gebruik een oudere versie van MariaDB. In ieder geval, ik ga voor de moeilijke manier :)

Op mijn online omgeving werkt het nog wel, dus ik wil die code eigenlijk intact laten. Daarin werk ik met het Entity Framework en mySQL nuget-packages. Zo kun je LINQ gebruiken: DBContext.Accounts.Where(rec => rec.Id == 3);
Een nieuw project gemaakt en daar het nuget-package MySqlConnector van Bradley Grainger aan toegevoegd. Dat werkt nog op de oude ADO manier, je maakt in code een connection-object aan, die open je, je maakt een command-object op basis van het connection-object aan, je vult deze met een query en met behulp van een data-adapter vul je een tabel. Of je gaat met een data-reader door je data.

De volgende stap is dat ik een Interface gemaakt heb. Waar ik nu in de code een fixed DBContext-class had, daar komt nu een eigen interface. Die krijgt dan dezelfde eigenschappen en methoden, maar je kunt zelf bepalen op een andere plek in je code welke concrete class je hiervoor gaat gebruiken. Nog even als toelichting, het gaat om een .NET Framework 4.7 project (bijgewerkt naar 4.8), dus het is (nog) niet een .NET Core project waar je op een makkelijker manier met "Dependency Injection" je implementatie-class kunt instellen.

Als je zelf met code en het Entity Framework gewerkt hebt, dan weet je dat je met DBContext voor de "database" en met DBSet objecten voor de "tabellen" werkt. Een DBSet is een abstracte class, die kun je niet zelf "aanmaken". Je moet (dus) zelf een class maken die hiervan erft. Hierna kopieer je alle functies uit de class DBSet en vervang je het woord "virtual" met "override".  Je ziet dat veel een "NotImplementedException" terug geven, omdat de concrete implementatie dit moet afhandelen. En dat gaan wij dus ook doen. De belangrijkste functies implementeer ik en daarmee krijg ik een werkende .Where(...)  en .ToList(...) functionaliteit. De asynchrone functies krijg ik zo niet werkend, maar dan doe ik eerst alleen wel even de synchrone versies. Dit is tenslotte nu alleen nog maar bedoeld om op mijn eigen laptop tijdens het ontwikkelen te kunnen testen of zaken werken.

Het is misschien niet exact hetzelfde, maar het aanpassen van deze code doet me een beetje terug denken aan Techorama, waarin Bart de Smet bij de laatste sessie zijn eigen InExpression toevoegt: link.

Het inloggen gaat goed, de validatie of mijn ingevoerde gebruikersnaam en wachtwoord overeenkomen met wat in de database staat, dat werkt. Alleen daarna wordt ik direct weer uitgelogd. Want na inloggen wordt in je database een soort timestamp opgeslagen (UPDATE actie) tot wanneer je sessie geldig is. En ook dat sessie-token wat in je headers meegestuurd wordt, wordt in de database opgeslagen. Ik moet hier dus ook de update-actie gaan uitvoeren. Dat valideer ik in mijn eigen implementatie van SaveChanges en SaveChangesAsync, naast een DBSet van de data die gebruikt (en bijgewerkt) wordt, heb ik een lijst die een kopie bevat van hoe de data in de database staat. Ik vergelijk deze records met elkaar, wat afwijkt, daar bouw ik het UPDATE sql-statement voor en voer ik uit op de database. Dat werkt.

Hierna kan ik na succesvol inloggen ook de data zien (aantal stappen, aantal gelopen kilometers). Dat loopt tot begin 2021, dus ik roep mijn cron-taak aan om de data aan te vullen vanuit de Fitbit-API. Ophalen gaat goed, het opslaan laat de applicatie crashen: een NotImplementedException. En dat was te verwachten, want de functie "Add(TEntity entity)" die ik gekopieerd heb uit DbSet<TEntity> bevat standaard de throw new NotImplementedException().

De fix hiervoor lijkt makkelijk, ik voeg de parameter entity toe in een eigen lijst en ga die dan met een INSERT actie in de database toevoegen binnen de SaveChanges en SaveChangesAsync functies. Maar dat lukt (helaas) niet. De functie "Add" verwacht namelijk een resultaat: een EntityEntry<TEntity>. Deze kun je met een .Create(..) aanmaken, maar de Create verwacht ook weer een object: een InternalEntityEntry. Dat is een abstracte class, als je die gaat implementeren, dan moet je de .base-constructor van InternalEntityEntry aanroepen en die verwacht 2 parameters: een IStateManager en een IEntityType.

Voordat ik nog dieper de code in duik (want die StateManager zal ook wel weer verplichte objecten in de constructor hebben) besluit ik om hier te stoppen. Het is niet de bedoeling dat ik de complete implementatie van DbSet ga nabouwen, dit is wat je noemt "een duik in het konijnenhol van Alice in Wonderland". Je wilt 1 dingetje doen en na een paar stappen ben je al bezig om een heel framework na te bouwen.

Ik voer dus toch een aanpassing in mijn code door. In het deel waar ik nu een .Add(...) uitvoer, daar plaats ik een try-catch(NotImplementedException) omheen. In het normale geval, dus zoals het nu live draait, gaat ie gewoon goed en rammelt je code wel door. Als er wel iets anders fout gaat, dan wordt de fout wel opgeworpen. Maar zit je in mijn code, dan krijg je dus een NotImplementedException en kan ik in dat catch-deel zelf een functie aanroepen waarin ik mijn eigen interne lijst bij houdt van records die met een INSERT in de database aangemaakt moet worden.

En na wat testen en tunen werkt dit ook!

Deze implementatie is op dit moment niet "productie-waardig". Bij de SaveChanges-/SaveChangesAsync-actie is het controleren of er data gewijzigd is "supertraag". Dat komt omdat ik nu stuk-voor-stuk de records vergelijk. Met Reflection dynamisch de velden en de waardes opvragen en vergelijken. In mijn applicatie heb ik 2 accounts, tot eind 2022 bijgewerkt heb ik ruim 6.100 records die ik ga vergelijken. En in de "cron-taak" doen we dat voor beide accounts: dus 2x. Dan zit je al gauw 10 tot 15 minuten te wachten tot de boel klaar is. En voor elk jaar wat erbij komt, komen er ook weer 365/366 records per jaar bij.

Daar heb je in DbSet dus de eigen afhandeling, die StateManager die per record bij kan houden of deze "added", "updated", "deleted" of "untouched" is.

Maar goed, ik kan nu wel op mijn eigen laptop mijn data binnen een docker container houden en daar ook een recente versie van MariaDB in draaien. En mocht de live-omgeving op een bepaald moment bijgewerkt worden zodat mijn code niet meer werkt, dan kan ik via een setting zo overgaan naar deze implementatie en werkt het weer. Want als gebruiker van mijn web-applicatie doe ik alleen maar lees-acties. De enige schrijfactie is op het moment dat ik inlog, maar omdat daar maar een paar records in die tabel staan, is dat nu geen  bottleneck.

Hier onder kun  je zien hoe de lijst getoond wordt nu ik de data bijgewerkt heb. Het is duidelijk dat 2020 en 2021 de "corona-jaren" waren, weinig uitstapjes, dus meer afstanden lopen, waaronder in 2020 en 2021 in september/oktober elke week een afstand van de Sallandse Wandelvierdaagse lopen. Dat tikt wel even aan. Ik wist dat ik vorige week wat extra mijn best heb gedaan, deze week niet echt aan extra rondjes lopen toe kwam en dat zie nu ook terug in de cijfers. En ook de verwachte eindstand is handig, op basis van de huidige afstand en aantal dagen wordt doorgerekend waar je (als je zo door gaat) ongeveer op uit komt. Het is duidelijk: ik mag nog even flink doorstappen!

Screenshot
lijst van kilometers en stappen per jaar, afgelopen weken en huidige week