Die Herausforderung der anonymen Typen bei LINQ-Projektionen

Der Dotnet-Doktor  –  0 Kommentare

Eine der zentralen Herausforderungen bei der Anwendung von Microsofts allgemeiner Abfragesprache LINQ sind die anonymen Klassen, die entstehen, wenn man nur die Ergebnismenge hinsichtlich der Attribute bzw. Spalten begrenzt. Einen anonymen Typ kann man nur als lokale Variable, nicht aber als Rückgabewerte von Methoden verwenden.

Eine Standardabfrage in LINQ nimmt im SELECT-Teil Bezug auf die Laufvariable. Dies ist gleichbedeutend mit einem "SELECT *" in SQL.

IEnumerable Ergebnis = from x in Menge where … select x;

Gerade bei der Abfrage von breiten Datenbanktabellen ist die Auswahl aller Spalten kein günstiges Verhalten. LINQ sieht daher vor, in dem SELECT-Teil durch eine sogenannte Projektion die Zielobjekte einzuschränken. Dabei entsteht ein sogenannter anonymer Typ. Ein anonymer Typ ist eine Klasse, die von dem Compiler bei der Übersetzung selbst erstellt und auch von dem Compiler benannt wird. Da der Typname zur Eingabezeit nicht bekannt ist, bieten die Sprachen C# und Visual Basic hier besondere Syntaxformen (var bzw. Dim ohne As):

C#: var Ergebnis = from x in Menge where … select new { x.a, x.b, x.c };

VB: Dim Ergebnis = from x in Menge where … select new with { x.a, x.b, x.c };

Die große Herausforderung besteht darin, dass anonyme Typen nur als lokale Variablen verwendet werden dürfen. Anonyme Typen können weder für Instanz- oder Klassenmitglieder noch für Rückgabewerte verwendet werden. Diese gravierende Einschränkung wird von vielen Benutzern kritisiert, aber Microsoft hat sogar schon angekündigt, dass man dies auch in C# 4.0 nicht ändern wird. Leider ist es auch nicht möglich, mit dem Cast-Operator, den LINQ bietet, eine Konvertierung eines anonymen Typs in eine vom Entwickler definierte Klasse zu vollziehen.

Es stellt sich also die Frage, wie man LINQ-Projektionen mit Kapselung und mehrschichtigem Entwickeln vereinbaren kann, wenn es doch nicht möglich ist, das Ergebnis einer Projektion an andere Methoden zu übergeben.

Es gibt folgende Möglichkeiten:

1. Möglich, aber sehr unschön ist die Rückgabe der anonymen Objekte mit dem Basistyp System.Object und die Typumwandlung in dem Aufrufer mit Hilfe eines gleich aufgebauten anonymen Typen in dem aufrufenden Programmcode (siehe Beispiel)

2. Wenn man LINQ-to-SQL einsetzt, kann man jeweils eigene typisierte Geschäftsobjektklassen im Datenkontext erzeugen, die nur eine Teilmenge der Tabelle aus der Datenbank abbilden. Dadurch sind Projektionen nicht mehr notwendig. Die Geschwindigkeit zur Laufzeit ist hoch, aber das Anlegen eigener Geschäftsobjektklassen ist sehr lästig, wenn viele Projektionen notwendig sind.

3. Wenn man LINQ-to-SQL einsetzt, kann man als Abfragesprache SQL verwenden anstelle von LINQ. Dabei ist das partielle Befüllen einer Geschäftsobjektklasse möglich. Es ist kurios, das Microsoft dieses partielle Befüllen für SQL, nicht aber für LINQ ermöglicht hat. IEnumerable Fluege5 = DB.ExecuteQuery("Select x.a, x.b. x.c from Tabelle as x where …"); Die Lösung ist performant, aber der Verzicht auf die LINQ-Syntax schmerzt.

4. Man kann sich einen eigenen Konvertierungsoperator als Erweiterungsmethode für die Schnittstelle IEnumerable schreiben, die für jedes Element der Eingabemenge eine Instanz der Zielklasse erzeugt und die Attribut per .NET-Reflection in die neue Instanz kopiert. Der Aufruf kann dann ganz elegant schreiben: IEnumerable Ergebnis2 = Ergebnis.Convert(). Den passenden Programmcode dafür finden Sie unten (*). Der Nachteil dieser Lösung ist der Geschwindigkeitsverlust durch den Einsatz von .NET-Reflection. Außerdem ist zu beachten, dass die kopierten Objekte vom Datenkontext losgelöst sind und diesem wieder mit Kontext.Table.AttachAll(Menge, false) hinzugefügt werden müssen. Weitere Ideen sind herzlich willkommen!

[CODE] (*) Listing zu der Erweiterungsmethode Convert()

/// <summary>

/// Erweiterung für LINQ-Projektionen

/// (C) www.IT-Visions.de Holger Schwichtenberg 2008

/// </summary>

internal static class WWWingsProjectionExtensions

{

/// <summary>

/// Konvertierung einer Menge eines (anonymen) Typs in einen anderen Typ

/// durch Abbildung gleichnamiger Attribute

/// </summary>

public static IEnumerable<T> Convert<T>(this IEnumerable Menge, System.Data.Linq.ITable Table)

where T : new()

{

// Neue Menge

List<T> Result = new List<T>();

// Duplizieren der enthaltenen Objekte

foreach (object o in Menge)

{

T e = CopyTo<T>(o);

Result.Add(e);

}

// Anfügen der neuen Menge an den Datenkontext

Table.AttachAll(Result, false);

return Result;

}



/// <summary>

/// Kopieren gleichnamiger Attribut auf ein anderes Objekt

/// </summary>

public static T CopyTo<T>(object from)

where T : new()

{

T to = new T();

return CopyTo<T>(from, to);

}



/// <summary>

/// Kopieren gleichnamiger Attribut auf ein anderes Objekt

/// </summary>

public static T CopyTo<T>(object from, T to)

where T : new()

{

Type FromType = from.GetType();

Type ToType = to.GetType();



// Kopieren der Fields

foreach (FieldInfo f in FromType.GetFields())

{

FieldInfo t = ToType.GetField(f.Name);

if (t != null)

{

t.SetValue(to, f.GetValue(from));

}

}

// Kopieren der Properties

foreach (PropertyInfo f in FromType.GetProperties())

{

object[] Empty = new object[0];

PropertyInfo t = ToType.GetProperty(f.Name);

if (t != null)

{

//Console.WriteLine(f.GetValue(from, Empty));

t.SetValue(to, f.GetValue(from, Empty), Empty);

}

}

return to;

}

} [/CODE]