Teil 3 von der Blog-Serie zum Thema LINQ Provider.
Bevor Sie hier weiterleisen
sollten sie die ersten 3 Teile gelesen haben. Teil 1: Hintergründe um Ziele
Teil 2: Basisklasse Query<T>
Teil 3: Basisklasse QueryProvider
Da wir nun die Basisklassen soweit abgesteckt haben können wir nun dazu übergehen uns um die Implementierung zu kümmern.
Wir brauchen daher als ersten Punkt einen Provider (abgeleitet von QueryProvider) der unsere abstrakte Funktionalität Provider.Execute(…) implementiert.
Außerdem müssen wir noch eine Klasse erstellen, die unsere Daten (woher diese auch immer stammen) typsicher repräsentiert.
Für dieses Beispiel verwende ich nun eine Klasse “Employee” ….
public class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public bool Active { get; set; }
}
… und den dazu passenden Provider …
public class EmployeeProvider : QueryProvider
{
public override object Execute(Expression expression)
{
//Kommt noch …
return null;
}
}
Außerdem ist es noch “hilfreich”, wenn wir uns einen DataContext (sie kennen so etwas eventuell aus LINQ-TO-SQL) erzeugen:
public class NorthwindDataContext
{
QueryProvider Provider = new EmployeeProvider();
public Query<Employee> Employees
{
get { return new Query<Employee>(Provider); }
}
}
Und somit können wir nun unsere ersten Abfragen gegen unsere eigene Datenquelle erzeugen.
var context = new NorthwindDataContext();
var res = from e in context.Employees
where e.Active
orderby e.LastName
select e;
Wenn Sie jetzt einen Breakpoint in die Provider.Execute Methode setzen und die Anwendung starten bekommen wir nun den von LINQ erzeugten ExpressionTree als Parameter in die Execute-Methode.
Diesen ExpressionTree gilt es nun noch auszuwerten, den gang zur Datenquelle zu machen und die Rückgabe zu erstellen.
Der von LINQ erstellte ExpressionTree enhält hierbei alle Information über die eigentliche Abfrage. Welche Felder inkludiert sind, welche Kritierien bei der Abfrage benötigt werden und auch welche Rückgabewerte erwartet werden.
Da – wie der Name schon sagt – der ExpressionTree eine Baumstruktur darstellt ist es oft notwendig eine Methode zu entwickeln die sich selbst aufruft und somit den kompletten Baum abarbeitet. Dabei wird eine Abfrage für die eigentliche Datenquelle erstellet.
In meinem Beispiel hab ich hierfür XML gewählt:
Die Execute(….) Methode bereitet einen LINQ Abfrage vor die wir entsprechend unseres ExpressionTrees in der ProcessExpression(….) Methode verändern/ersetzen.
public override object Execute(Expression expression)
{
//Create query
var doc = XDocument.Load(file);
var query = from emp in doc.Descendants("Employee")
select emp;
//Edit query
ProcessExpression(expression, ref query);
//Transform Data
var res = GetElementsFromXml(query);
//Return Data
return res;
}
In ProcessEpxression editieren wir nun die Linq To Xml Abfrage (natürlich könnte man hier auch einen SQL Abfrage zusammensetzen mit einem StringBuilder etc.) und haben am Ende die fertige Abfrage für die XML Datei.
ACHTUNG: Dies hier sollte lediglich einen Ansatz darstellen. In der Praxis wäre noch jede Menge zu tun. (mehere Bedinungen richtig verknüpfen, optimierungen f. d. Abfrage, implementieren aller Operatoren …)
private void ProcessExpression(Expression e, ref IEnumerable<XElement> resultQuery)
{
if (e is UnaryExpression)
{
var exp = e as UnaryExpression;
ProcessExpression(exp.Operand, ref resultQuery);
}
else if (e is LambdaExpression)
{
var exp = e as LambdaExpression;
ProcessExpression(exp.Body, ref resultQuery);
}
else if (e is BinaryExpression)
{
var exp = e as BinaryExpression;
if (exp.NodeType == ExpressionType.Equal)
{
string attrib = string.Empty;
object value = null;
if (exp.Left is MemberExpression)
{
var left = exp.Left as MemberExpression;
attrib = left.Member.Name;
value = Expression.Lambda(exp.Right).Compile().DynamicInvoke();
}
else if (exp.Right is MemberExpression)
{
var right = exp.Right as MemberExpression;
attrib = right.Member.Name;
value = Expression.Lambda(exp.Left).Compile().DynamicInvoke();
}
else
{
throw new NotImplementedException();
}
//ChangeTargetExpressionTree
resultQuery = from emp in resultQuery
where emp.Attribute(attrib).Value == GetValueFromMapping(attrib, value).ToString()
select emp;
}
}
else if (e is MethodCallExpression)
{
var exp = e as MethodCallExpression;
string attrib, value = string.Empty;
if (exp.Type == typeof(IOrderedQueryable<Employee>))
{
attrib = (((exp.Arguments[1] as UnaryExpression).Operand as LambdaExpression).Body as MemberExpression).Member.Name;
resultQuery = from emp in resultQuery
orderby emp.Attribute(attrib).Value
select emp;
}
if (exp.Method.DeclaringType == typeof(string))
{
value = Expression.Lambda(exp.Arguments[0]).Compile().DynamicInvoke().ToString();
attrib = (exp.Object as MemberExpression).Member.Name;
switch (exp.Method.Name)
{
case "Contains":
resultQuery = from emp in resultQuery
where emp.Attribute(attrib).Value.Contains(value)
select emp;
break;
case "StartsWith":
resultQuery = from emp in resultQuery
where emp.Attribute(attrib).Value.StartsWith(value)
select emp;
break;
default:
throw new NotImplementedException();
}
}
else
{
foreach (var current in exp.Arguments)
{
ProcessExpression(current, ref resultQuery);
}
}
}
}
Damit haben wir unseren ersten lauffähigen Provider und können die Anwendung testen.
Im 5ten und letzten Teil der Serie werden wir den Provider noch mit funktionalitäten ausstatten, um auch Änderungen speichern zu können (ChangeTracking).