Dernièrement j'ai eu l'occasion de voir une video très sympa parlant d'un projet CodePlex que j'avais un peu oublié : LinqToSharePoint. Pour ceux qui ne connaitrait pas, cet outil, crée par Bart De Smet (Ancien MVP C#, Employé Microsoft depuis peu), permet de convertir des requêtes Linq en CAML.
Alors cela fait maintenant près de 8 mois que Thomas vous a parlé de ce projet plein de promesses, il est temps de voir ce qui a été fait :
Histoire de tester, on va exporter des données de la base NorthWind dans SharePoint pour obtenir ces trois listes : Products, Categories et Suppliers
L'idée étant qu'un produit est lié à un supplier et à une catégorie. Celà se traduit par une clef étrangère dans la base et par un champ de type "lookup" dans SharePoint.
Une fois l'export terminé et LinqToSharePoint installé, vous avez la possibilité de rajouter ce nouvel élément à votre projet : Linq to SharePoint file
Ce fichier va vous permettre de configurer votre accès aux données grâce à un assistant.

Vous l'aurez compris cet accès se fait par WebServices. Il vous suffit maintenant de spécifier les champs qui vous intéressent et vous pouvez vous mettre à coder !

Prenons un exemple pratique : Supposons que je souhaite récupérer tout les produits dont l'Unit Price est supérieur à 100, en triant le tout par ordre Décroissant. Pour au final, afficher le nom du produit et son prix.
En CAML, ca donnerait ceci (grosso modo, pas de gestion d'erreurs, etc.):
System.Text.StringBuilder oSbQuery = new System.Text.StringBuilder();
oSbQuery.Append(" <Where>");
oSbQuery.Append(" <Gt>");
oSbQuery.Append(" <Value Type=\"Currency\">100</Value>");
oSbQuery.Append(" <FieldRef Name=\"UnitPrice\" />");
oSbQuery.Append(" </Gt>");
oSbQuery.Append(" </Where>");
oSbQuery.Append(" <OrderBy>");
oSbQuery.Append(" <FieldRef Name=\"UnitPrice\" Ascending=\"FALSE\" />");
oSbQuery.Append(" </OrderBy>");
string sResultQuery = oSbQuery.ToString();
System.Text.StringBuilder oSbViewFields = new System.Text.StringBuilder();
oSbViewFields.Append(" <FieldRef Name=\"Title\" />");
oSbViewFields.Append(" <FieldRef Name=\"UnitPrice\" />");
string sResultViewFields = oSbViewFields.ToString();
SPQuery query = new SPQuery();
query.ViewFields = sResultViewFields;
query.Query = sResultQuery;
using (SPWeb web = new SPSite("http://localhost/NorthWind").OpenWeb())
{
SPList products = web.Lists["Products"];
SPListItemCollection items = products.GetItems(query);
foreach (SPListItem item in items)
{
Console.WriteLine(String.Format("{0} supplied by {1}", item["ProductName"], item["UnitPrice"]));
}
}
en Linq, ça devient :
var ctx = new SPNorthWindSharePointDataContext();
var req_Products_1 = from p in ctx.Products
where p.UnitPrice > 100
orderby p.UnitPrice descending
select new { Product = p.ProductName, p.UnitPrice };
foreach (var p in req_Products_1)
{
Console.WriteLine(String.Format("{0} costs {1}", p.Product, p.UnitPrice));
}
Desuite, ca fait une sacrée différence :). Et l'affichage des informations est bien plus simple à faire vu q'on travaille directement avec des objets typés.
Dans le même style, voilà une autre requête, où l'on souhaite récupérer tout les produits dont le ProductName commence par C. On remarquera qu'en CAML, il faudra passer le nom interne de la colonne ProductName (à savoir, Title), LinqToSharePoint fait la traduction tout seul.
<Query>
<Where>
<BeginsWith>
<FieldRef Name="Title" />
<Value Type="Text">C</Value>
</BeginsWith>
</Where>
<OrderBy>
<FieldRef Name="Title" Ascending="FALSE" />
</OrderBy>
</Query>
<ViewFields>
<FieldRef Name="Title" />
<FieldRef Name="UnitPrice" />
</ViewFields>
var req_Products_2 = from p in ctx.Products
where p.ProductName.StartsWith("C")
orderby p.ProductName descending
select new { Product = p.ProductName, p.UnitPrice };
Bon c'est sympa, mais soyons honnête, il existe beaucoup d'outils pour simplifier la génération de CAML, qu'est que celui ci apporte en plus ?
Encore une fois, voyons par la pratique : Je souhaite récupérer tout les produits dont la catégorie est Beverages et dont le ProductName commence par C.
En CAML, si on veut faire cela proprement, cela nécessitera deux requêtes.
<Query>
<Where>
<Eq>
<Value Type="Text">Beverages</Value>
<FieldRef Name="Title" />
</Eq>
</Where>
</Query>
<ViewFields>
<FieldRef Name="ID" />
</ViewFields>
et
<Query>
<Where>
<And>
<BeginsWith>
<FieldRef Name="Title" />
<Value Type="Text">C</Value>
</BeginsWith>
<Eq>
<Value Type="Lookup">1</Value>
<FieldRef Name="Category" LookupId="TRUE" />
</Eq>
</And>
</Where>
<OrderBy>
<FieldRef Name="Title" Ascending="FALSE" />
</OrderBy>
</Query>
<ViewFields>
<FieldRef Name="Title" />
<FieldRef Name="UnitPrice" />
</ViewFields>
Ca commence à devenir un peu long à écrire tout cet XML, juste pour une reqête relativement simple non ? Heureusement avec LinqToSharePoint ca donne ça :
var req_Products_3 = from p in ctx.Products
where p.ProductName.StartsWith("C")
&& p.Category.CategoryName == "Beverages"
orderby p.ProductName descending
select new { Product = p.ProductName, p.UnitPrice };
Desuite, on voit la différence ! En interne, la requête est exactement celle que vous avez vu plus haut. LinqToSharePoint s'occupe seul de faire le lien. Notamment par l'utilisation de l'élément Patch dans la requête ci-dessous.

Bon attention, vous ne pouvez pas non plus faire n'importe quoi avec. Par exemple, l'utilisation de la méthode EndsWith n'est pas supporté. Cette restriction étant du au CAML en lui même.
Bien sur il est possible de contourner cette limitation en encastrant la requête ci-dessous dans une autre requête Linq classique mais par contre ca complexifie l'ensemble.
Et pour finir, sur cette petite démonstration de ce version ALPHA, voilà la requête qui m'a fait installer ce tool sur ma VPC.
Comment faire pour afficher le nom de tout les produits commençant par C, dont la catégorie est Beverages et en plus d'afficher le CompanyName du Supplier ? Evidemment en CAML, ca va commencer à faire une sacré tranche d'XML (dont je vous épargne le Copier/Coller) mais en Linq, cela reste relativement simple (on a même plusieurs façons de pouvoir écrire la requête).
var req_Products_4 = from p in ctx.Products
where p.Category.CategoryName == "Beverages"
&& p.ProductName.StartsWith("C")
orderby p.ProductName descending
select p;
foreach (var p in req_Products_4)
{
Console.WriteLine(String.Format("{0} supplied by {1}", p.ProductName, p.Supplier.CompanyName));
}
var req_Products_5 = from p in ctx.Products
where p.Category.CategoryName == "Beverages"
&& p.ProductName.StartsWith("C")
orderby p.ProductName descending
select new { Product = p.ProductName, p.Supplier.CompanyName };
foreach (var p in req_Products_5)
{
Console.WriteLine(String.Format("{0} supplied by {1}", p.Product, p.CompanyName));
}
Bien entendu, l'ensemble de ces requêtes sont optimizées afin d'éviter les appels intempestifs à SharePoint. Par exemple, Si vous avez 4 produits appartenant à la même catégorie, il n'effectuera la requête pour obtenir l'ID de la catégorie qu'une seule fois
Par contre, il n'est pas possible de mettre à jour les données via LinqToSharePoint mais apparement, ca ne saurait tardé.
LinqToSharePoint fonctionne aussi bien avec du C# 3.0 que du VB 9.0 et il est possible de se connecter aussi bien par WebServices que par le Modèle Objet.
<Philippe/>