EFCore: Wie baue ich einen globalen Converter?

Kokujou

Lieutenant
Registriert
Dez. 2017
Beiträge
929
Hallihallo :)

Folgendes Szenario. Um Type-Safety zu gewährleisten und nervige String-Konstanten zu umgehen habe ich in einem Projekt eine Art improvisiertes enum gezaubert und zwar in der Ausführung für string und für guid. Es gibt eine Basis-Klasse z.B. GuidEnum. Die sieht etwa soa su:


C#:
public class GuidEnum
{
        private readonly Guid _value;

        public GuidEnum(string value)
        {
            _value = Guid.Parse(value);
        }

        public static implicit operator string(GuidEnum t)
        {
            return t._value.ToString();
        }

        public static implicit operator Guid(GuidEnum t)
        {
            return t._value;
        }

        public static T Parse<T>(Guid value) where T : GuidEnum
        {
            //Reflections are fun
        }
    }

}

Nun ein Problem. wnen ich diese Typen in EFCore benutze fällt er erwartungsgemäß auf die Nase.
ich kenne die HasConversion Eigenschaft von Properties, aber dann müsste man für jede Instanz dieser Enum-Klassen diese HasConversion einbauen und das ist ja schonmal schrecklich redundant.

Es gibt einen Reflections-Code mit dem man immerhin über einen For-Loop diese Konverter für alle betroffenen typen in allen deifnierten entties definieren kann - ABER!

genau das funktioniert in meinem Fall nicht. Warum?
1. EFCore scheint meine Property völlig zu verunstalten und statt des abgeleiteten typen, z.B. StatusId : GuidEnum erhalte ich sowas wie StatusIdTemplateId.
2. will ich die Konverter nicht für jeden Konkreten typen definieren sondern es generisch machen. Wenn der BaseType der Property GuidEnum ist, möchte ich einen generischen Konverter mit dem Property-Type als Type-Argument erstellen und ihn der Property zuordnen. Das funktioniert aber so gar nicht, denn der BaseType ist in meinem Fall plötzlich "ValueType".

für MVC Newtonsoft.Json funktioniert das herrlich simpel. und wenig komplex, also hatte ich gehofft bei EFCore gibt es das auch... also dachte ich ich frag mal lieber in die Community ob jemand mit sowas schonmal zutun hatte, oder ob das mal wieder eine unlösbare Aufgabe ist^^
 
Das ganze klingt nach einem XY-Problem. Ich dir keine Lösung für dein aktuell konkretes Problem nennen, aber wenn ich schon "Zaubern" und "Programmieren" zusammen sehe, gehen bei mir alle Alarmglocken an.

Metaprogrammierung mit Reflection und ähnlichem ist bis auf wenige Ausnahmen absoluter Code-Smell.

Was ist dein wirkliches Ziel? Und was ist der Sinn der Klasse? Das habe ich nicht verstanden.
 
  • Gefällt mir
Reaktionen: PHuV
Definitiv XY-Problem, das zeigt schon der erste Satz. Ich würde es auch für sinnvoll erachten, wenn du genauer darauf eingehst, was aus deiner Sicht das Problem mit der Type-Safety ist.
 
Wir haben in der Datenbank halt sehr viele konstanten definiert. Was ja per se schon ein richtig übler Code-Smell ist. Wir vernwenden konstante Guids, die dann einem String-Wert zugeordnet sind und so weiter.

um das im Code abzubilden bräuchten wir dann mehrere Konstantenklassen, die diese werte dann sammeln. Soweit so gut, das kann man so machen. Aber konstanten im code sind eigentlich genauso ein Code-Smell. Denn wenn man einen fest definierten Wertebereich hat möchte man natürlich Enums haben.

Diese Klassen sind also meine Art eine Art Konstante-zu-Enum Mapping durchzuführen. Ansonsten hast du eine Funktion wo sowas steht wie DoSomeThing(Guid statusId) und der Programmierer der drauf guckt muss jetzt raten in welchen der unzähligen Konstanten-Klassen er die Guid findet. Schöner wäre aber

DoSomeThing(StatusId statusId) wo dann direkt IntelliSense auch vorschlägt StatusId Member zu wählen an statt nur Guid.Parse oder Guid.Empty
Ergänzung ()

Bagbag schrieb:
Metaprogrammierung mit Reflection und ähnlichem ist bis auf wenige Ausnahmen absoluter Code-Smell.
das seh ich eigentlich genauso, aber wir haben einige Fälle wo wir nicht drumrum kommen. aktuell z.B. zur dynamischen Generierung eines Konverters fürs JSON damit wir nicht für jeden StringEnum typen einen neuen konverter schreiben und registrieren müssen, das wäre wirklich schrecklich.
 
  • Gefällt mir
Reaktionen: PHuV und Staubgeborener
Bagbag schrieb:
Ansonsten wäre eine andere Herangehensweise die Datenbank umzubauen.
schön wärs... aber ne 10 jahre alte datenbank kann man nicht mehr austauschen ohne den entwicklungsfortschritt für jahre aufzuhalten >.< zumindest hat da jeder entwickler den ich deswegen gefragt habe den daumen drauf ^^
Bagbag schrieb:
falls ich da nichts überlesen habe beschreibt dieser artikel nur wie ich die Property.HasConversion funktion benutze.
Aber wir haben unzählige instanziierungen dieses Objekts. Ich will auf keinen Fall für jede Instanziierung eine .HasConversion funktion machen, das ist sehr unschön. Darum frage ich hier ja auch, wie ich diese HasConversion-sache global definieren kann.

Ich meine es muss doch machbar sein einen default converter zu basteln, der immer aktiv wird wenn man versucht ein Objekt von DB Typ A in C# Typ B zu konvertieren, darum geht es doch. Bei JSON funktioniert das Problemlos, nur EFCore stellt sich an :(

selbst die "gehackten" lösungen sehen vor dass ich im prinzip durch alle entites und dann durch jeweils ihre properties durchiteriere und die entsprechende Property suche nur um ihr dann den konverter zu verpassen. Also dieselben steps nur weniger code.
Aber über die EFCore reflections bekomme ich ja nichtmal den richtigen C#-Typ. Stattdessen verunstaltet EFCore lieber diesen Typ und ersetzt den BaseType gleich mit :(
 
Also für mich klingt das so, als hättet ihr etwas in der Datenbank, was sich nicht mehr verändert? Falls dem so ist, erstelle dir doch einfach ein Programm, was dir den Code aus der vorhandenen Datenbank generiert, und zwar nicht zur Laufzeit (reflection), sondern bevor du es kompilierst (als echte *.cs Dateien nach einer Konvention).

Falls die Datenbank sich zur Laufzeit des Programms ändert, könntest du deinen Typ GuidEnum so umbauen, dass er eine Lambda-Function im Konstruktor übernimmt, wo du die Konversion zur Laufzeit definieren kannst. Simpel und dadurch wäre nur der Teil der Konversion zu implementieren. Außerdem könntest du durch Überladen eine Standard-Lambda festlegen, der dann die meisten wiederkehrenden Fälle abdeckt.
 
marcOcram schrieb:
Für EF Core 6 kannst du einen Standard definieren: https://github.com/dotnet/efcore/issues/10784#issuecomment-858022501
Bei EF Core 5 bleibt dir nur der Umweg es selbst zu basteln bzw. es für jede Property selbst zu setzen (Suchen und Ersetzen mit Regex kann helfen).
gut zu wissen... das könnte ich ins auge fassen und darauf bauen. das ist wirklich gut. danke!
sandreas schrieb:
Also für mich klingt das so, als hättet ihr etwas in der Datenbank, was sich nicht mehr verändert? Falls dem so ist, erstelle dir doch einfach ein Programm, was dir den Code aus der vorhandenen Datenbank generiert, und zwar nicht zur Laufzeit (reflection), sondern bevor du es kompilierst (als echte *.cs Dateien nach einer Konvention).

Falls die Datenbank sich zur Laufzeit des Programms ändert, könntest du deinen Typ GuidEnum so umbauen, dass er eine Lambda-Function im Konstruktor übernimmt, wo du die Konversion zur Laufzeit definieren kannst. Simpel und dadurch wäre nur der Teil der Konversion zu implementieren. Außerdem könntest du durch Überladen eine Standard-Lambda festlegen, der dann die meisten wiederkehrenden Fälle abdeckt.
Die konstanten selbst ändern sich größtenteils nicht. sehr wohl aber die datensätze. Man muss sich das so vorstellen dass die datenbank auf verschiedenen systemenl iegt, genau wie unser code. Man fügt ja Benutzer und Produkte und wer weiß was noch alles hinzu (ich halt mich hier mal lieber möglichst bedeckt, schweigen ist der beste datenschutz :p)
ich wneiß nicht genau ob ich deine lösung genau verstanden habe. Das Problem ist wenn ich ein Modell habe mit einer Property die einen Komplexen Typ hat, muss EFCore irgendwie wissen dass er dem Primitiven Datenbank-Typ dieses komplexe objekt zuordnen kann. Mehr brauch ich nicht.

Ein User hat also eine Guid xxx, die wiederum in der DB irgendwo den Wert "aktiv" hat.
Und das ganze gewäsch möchte ich weglassen und sagen User.Status = StatusId.Aktiv

kA wie ich in dieses Konzept Lambda-Funktionen reinbringen soll, vielleicht kannst du ja kurz ein minimales Pseudocode-Beispiel schreiben
 
Also möchtest du ein bisher dynamisches System (zur Laufzeit können neue Typen definiert werden) jetzt hardcoden?

Das klingt mir eher nach reingehackt.
 
Bagbag schrieb:
Also möchtest du ein bisher dynamisches System (zur Laufzeit können neue Typen definiert werden) jetzt hardcoden?

Das klingt mir eher nach reingehackt.
Ich versuchs nochmal kurz und bündig zu formulieren.

Ich habe unzählige Entities. Diese Entities haben eine Property. Und diese Property ist vom Typ GUID, hat aber einen festen Wertebereich.

Beispiel: ein User hat eine Property Status, die so definiert ist:

Status Id Status Name
ff5e5ea6-0051-49a5-a8ad-a313702ce618 Aktiv
5cba1815-208d-4384-9919-86ca0ef98716 Inaktiv

Ich möchte jetzt also eine klasse bauen die so aussieht:


C#:
public class StatusId {
    public static readonly Aktiv = new ("ff5e5ea6-0051-49a5-a8ad-a313702ce618");
    public static readonly Inaktiv = new ("5cba1815-208d-4384-9919-86ca0ef98716");
}

das ganze wird dann durch eine implizite Konvertierung in den typ Guid und einer private readonly variable _value gestützt, wie ich sie oben schonmalg ezeigt habe.

Wenn man dann in einer Funktion einen Status brauchst kannst du einfach diese Klasse dort verlinken. Es ist also nicht möglich dort eine neue Guid einzugeben und somit falsche Werte zu erzeugen, oder aber irgendeine andere Guid, die nicht im unterstützten Wertebereich liegt. sondern eben nur diese beiden Status-Guids.

Soweit ich weiß ist das ein C#-Konzept dass sehr populär ist, ein Value-Wrapper nur halt noch zusätzlich mit eingeschränktem Wertebereich.

aber wie bereits erwähnt gibt es Stand EFCore 5 noch keine Funktion mit der ich eine Standard-Konvertierung für diese neue Klasse StatusId einbauen kann und genau das ist die Frage :)

Klar soweit?
 
Ja, was du vor hast hatte ich verstanden.

Ist es vorgesehen, dass man die möglichen Status noch erweitert (und es deshalb in der Datenbank definiert wurde)?
 
Bagbag schrieb:
Ja, was du vor hast hatte ich verstanden.

Ist es vorgesehen, dass man die möglichen Status noch erweitert (und es deshalb in der Datenbank definiert wurde)?
soweit ich weiß nicht. das system ist schon ziemlich alt und wird nur noch auf verschiedene plattformen migriert bzw bugfixes werden rausgehauen aber eine erweiterung daran glaube ich nicht. und sicher nicht was DB konstanten angeht...
 
Worauf ich hinaus wollte ist, dass es vielleicht gar keine konstanten sind sondern mehrere Status, die ursprünglich erweiterbar sein sollten und sich auch zb von deployment zu deployment unterscheiden könnten.

Wenn beides nicht der Fall ist, ist dein Weg wohl sinnvoll.
 
Bagbag schrieb:
Worauf ich hinaus wollte ist, dass es vielleicht gar keine konstanten sind sondern mehrere Status, die ursprünglich erweiterbar sein sollten und sich auch zb von deployment zu deployment unterscheiden könnten.

Wenn beides nicht der Fall ist, ist dein Weg wohl sinnvoll.
achso... nein dem ist nicht so... es geht wirlich nur um konstante werte, also nicht sowas wie zuordnungen von dynamischen werten sondern im wahrsten Sinne des Wortes:
aktiv, inaktiv, in bereitschaft, wartend, abgebrochen
 
Ja, aber auch da könnte doch noch was hinzu kommen.

Beispiel mit Versandarten:
Betrieb A bietet nur normalen Versand an und Betrieb B hat zusätzlich noch Expressversand. Irgendwann kommt Betrieb A auf die Idee Same-Day Lieferung anzubieten.

Wird sich auch quasi nie ändern, sind also "praktisch" konstanten, muss aber trotzdem dynamisch sein, wenn man es nicht hardcoden will (weil sich es sonst in den Betrieben überschneidet).
 
Morgen ihr alle :)

Also ich hab jetzt eine "Lösung" gefunden. Für alle die auf sowas wie Code-Smells achten, bitte eine Nasenklammer aufsetzen. Das ist vermutlich der schlimmste Reflections-Code den ich je schreiben musste, aber damit kriegt ihr einen präziesen Eindruck was ich machen will. Und wenn irgendjemand eine bessere Lösung kennt, notfalls auch mit EF6, dann bitte raus damit:


C#:
var type = typeof(ConfigDatabaseContext);
var definedModelTypes = type.GetProperties(BindingFlags.Public | BindingFlags.Instance |
                                           BindingFlags.GetProperty |
                                           BindingFlags.SetProperty)
    .Where(x => x.PropertyType.IsGenericType &&
           x.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>))
    .Select(x => x.PropertyType.GenericTypeArguments[0]);

foreach (var modelType in definedModelTypes)
{
    var customEnumProps = modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance |
                                                  BindingFlags.GetProperty | BindingFlags.SetProperty)
        .Where(x => x.PropertyType.BaseType == typeof(GuidEnum) ||
               x.PropertyType.BaseType == typeof(StringEnum));
    foreach (var customEnum in customEnumProps)
    {
        var converterType = typeof(GuidToGuidEnumConverter<>).MakeGenericType(customEnum.PropertyType);
        var converter = (ValueConverter) Activator.CreateInstance(converterType);
        modelBuilder.Entity(modelType).Property(customEnum.Name).HasConversion(converter);
    }
}

Und nochmal in Textform für Leute die nicht so firm mit Reflections sind:
1. man hole sich sämtliche öffentlichen Properties des DBContext die ein DBSet sind
2. Man hole sich das generische Typargument aus jedem dieser DBSets
3. Man hole sich alle öffentlichen Properties mit Typ GuidEnum oder StringEnum all dieser Typen
4. Nun kann man die ModelBuilder.Entity(modelType).Property(propertyName) aufrufen
5. Man erstelle nun eine generische Instanz eines GuidEnum Konverters mit dem PropertyType als Typ-Argument
6. Man rufe die HasConversion methode für Schritt 4 mit dem Konverter aus Schritt 5 auf
 
Zuletzt bearbeitet:
Zurück
Oben