LINQ Coding Guidelines #4–Mapping von Daten separieren

9. Dezember 2014

Mit dem letzten Beitrag hatte ich Einrückungen und Umbrüche adressiert. Die dort verwendeten Beispiele kann man aber noch deutlich durch eine weitere Maßnahme verbessern…

Empfehlung:
Mapping von Daten sollte aus LINQ-Ausdrücken herausgehalten werden.

 

Nochmal zur Erinnerung die Beispiele:

   1: public ApiExpert Get(int id)

   2: {

   3:     return this.db.eXperts

   4:         .Where(x => x.Id == id)

   5:         .ToArray()  

   6:         .Select(x => new ApiExpert

   7:         {

   8:             Id = x.Id,

   9:             IsAdmin = x.IsAdmin,

  10:             Name = x.Name,

  11:             CustomerResponse = x.CustomerStatements.Select(c =>

  12:                 new KeyValuePair<string, string>(c.Id.ToString(CultureInfo.InvariantCulture), c.Statement)

  13:                 )

  14:         })

  15:         .FirstOrDefault();

  16: }

  17: public IEnumerable<ApiExpert> Get()

  18: {

  19:     return this.db.eXperts

  20:         .Select(x => new ApiExpert

  21:         {

  22:             Id = x.Id,

  23:             IsAdmin = x.IsAdmin,

  24:             Name = x.Name,

  25:         });

  26: }

Sowie:

   1: IQueryable<AllocationTimeSpan> allTimeSpans = _context

   2:     .AllocationSet

   3:     .OfType<Allocation>()

   4:     .Select(a => new AllocationTimeSpan

   5:     {

   6:         From = a.AllocatedFrom,

   7:         To = a.AllocatedTo,

   8:         Status = (Status)(a.Status),

   9:         AvailableMDRatio = a.AvailableMDRatio

  10:     })

  11:     .Distinct()

  12:     .OrderBy(ts => ts.From);

Beiden Beispiele ist gemein, dass der Ausdruck nicht von der Abfragelogik, sondern vom Mapping der Daten dominiert wird. Das ist aus mehreren Gründen schlecht:

  • In der Regel ist die Abfragelogik wichtiger für das Verständnis, als die Details des Mappings
  • Das Mapping wird im Detail ausimplementiert (also das “wie”), anstatt einfach (deklarativ!) das “was” hinzuschreiben.
  • Die Details des Mappings werden so normalerweise redundant über den Code verteilt. Das ist natürlich bei Änderungen alles andere als wartungsfreundlich.
  • Sollte es unterschiedliche Mappings geben – zum Beispiel weil in unterschiedlichen Kontexten unterschiedliche Ausschnitte der Daten benötigt werden – ist das im Code nicht erkennbar.

Leider ist es so, dass Microsoft das ganz gerne in seinen Beispielen vormacht, die meisten Entwickler finden das also nicht sonderlich problematisch. Das heißt aber nicht, dass man es nicht besser machen könnte…

Zum Beispiel durch die Einführung von Mapper-Klassen, die sich dediziert um das Mapping der Daten kümmern:

   1: static class ExpertsMapper

   2: {

   3:     public static ApiExpert MapToUIForList(this eXperts x)

   4:     {

   5:         return new ApiExpert

   6:         {

   7:             Id = x.Id,

   8:             IsAdmin = x.IsAdmin,

   9:             Name = x.Name,

  10:         };

  11:     }

  12:  

  13:     public static ApiExpert MapToUIForDetails(this eXperts x)

  14:     {

  15:         return new ApiExpert

  16:         {

  17:             Id = x.Id,

  18:             IsAdmin = x.IsAdmin,

  19:             Name = x.Name,

  20:             CustomerResponse = x.CustomerStatements.Select(c => new KeyValuePair<string, string>(c.Id.ToString(CultureInfo.InvariantCulture), c.Statement))

  21:         };

  22:     }

  23: }

Konsequent im ersten Beispiel eingesetzt:

   1: public ApiExpert Get(int id)

   2: {

   3:     return this.db.eXperts

   4:         .Where(x => x.Id == id)

   5:         .ToArray()

   6:         .Select(x => x.MapToUIForDetails())

   7:         .FirstOrDefault();

   8: }

   9: public IEnumerable<ApiExpert> Get()

  10: {

  11:     return this.db.eXperts.Select(x => x.MapToUIForList());

  12: }

Damit lassen sich die genannten Probleme in den Griff bekommen:

  • Die Abfrage ist auf die Logik reduziert, nicht die Details des Mappings.
  • Die Logik des Mappings ist zentralisiert kann bei Änderungen leichter angepasst werden.
  • Durch die Namensgebung der Methoden wird die Intention klar.

 

Wenn das Mapping der Daten komplexer wird, etwa weit Unterabfragen ins Spiel kommen, Nullwerte geprüft oder Datentypen umgewandelt werden müssen, wird der Gewinn noch deutlicher.

Allerdings gibt es eine Grenze dort, wo der LINQ-Provider Informationen verliert. Ist das ein durchgängiger LINQ-Ausdruck, kann der Provider das womöglich über JOINs effizienter abhandeln, als wenn jedes Mapping eine separate Abfrage ausführt. Ob ein Feld mehr oder weniger abgefragt wird ist aber i.d.R. kein Problem.