Service WCF orienté données Agile avec EF4 et “faux” provider LINQ
Contrairement à l’approche de Julie, je suis parti sur une approche Self-Tracking Entities et T4 à fond.
La première étape consiste créer un projet que nous appellerons DAL et dans lequel, on va intégrer un edmx.
Dans mon exemple, je suis parti sur Northwind avec seulement les tables Customers, Orders et [Order Details].
Une fois cela fait, on va rajouter un nouvel item de type ADO.NET Self-Tracking Entity Generator à notre projet.
Cela aura pour effet de générer deux templates de génération de code T4 :
- un qui contiendra les entités + des classes / interfaces destinées au Self-Tracking
- un qui contiendra notre contexte + une classe d’extension methods.
Nous allons déplacer notre template d’entités dans un projet dédié : Entities.
Après avoir rajouté la référence vers Entities à notre projet DAL, nous allons définir dans nos templates le chemin vers l’edmx.
Ensuite, nous allons définir un projet Repositories. Dans ce projet, nous allons définir une interface INorthwindRepository.
Vous pouvez remarquer la redondance de INorthwindRepository. C’est pourquoi, dans mon exemple, ces deux interfaces sont générées avec un template T4 se basant sur mon edmx. Ainsi, même si je change de modèle de données, je n’aurai rien à changer, juste à regénérer le code de mes templates.
Maintenant, je vais revenir sur notre contexte. En effet, je vais lui faire implémenter INorthwindRepository. Pour cela, je vais modifier le template T4 de manière à obtenir la classe suivante :
Qui dit service WCF dit Service Contract. Je vais donc me créer deux nouveaux projets : Services et ServiceContracts (basés tous les deux sur T4).
Je rappelle qu’à ce moment là, si vous avez déjà les templates T4 (réutilisables d’un projet sur l’autre), vous n’avez toujours pas écrit une ligne de code.
On peut constater l’indépendance des assemblies vis-à-vis de Entity Framework (à l’exception de la couche DAL bien sûr) :
Voulant utiliser Unity, j’ai ensuite choisi l’approche proposée par Alexey Zakharov. J’ai donc intégrer son projet WCFFacility et les classes Bootstrapper et UnityServiceLocatorAdapter dans mon projet WCFService. Une fois le svc et le fichier de config définit, mon service est terminé !
Maintenant côté client, je voulais rajouter l’utilisation d’un “faux” provider LINQ. Pour cela, je me suis basé sur ce que j’avais déjà fait précédemment lors d’une pres pour la communauté ALT.NET française.
Je me suis d’abord créé un projet Client.LINQ dans lequel j’ai défini deux classes : ClientLINQ et MyQueryable.
public static class ClientLINQ
{
public static MyQueryable<T> Where<T>(this MyQueryable<T> source, Expression<Func<T, bool>> where)
{
source.WhereValue = string.Concat(source.WhereValue ?? "", where.Body.ToString().Replace(string.Format("{0}.", where.Parameters[0].Name), "it.").Replace("\"", "'").Replace("||", " OR ").Replace("&&", " AND "));
return source;
}
public static MyQueryable<T> OrderBy<T, T2>(this MyQueryable<T> source, Expression<Func<T, T2>> orderBy)
{
source.OrderByValue = orderBy.Body.ToString().Replace(string.Format("{0}.", orderBy.Parameters[0].Name), "it.");
return source;
}
public static MyQueryable<T> Include<T>(this MyQueryable<T> source, string include)
{
source.IncludeValues.Add(include);
return source;
}
public static MyQueryable<T> Skip<T>(this MyQueryable<T> source, int number)
{
source.SkipValue = number;
return source;
}
public static MyQueryable<T> Take<T>(this MyQueryable<T> source, int number)
{
source.TakeValue = number;
return source;
}
public static T FirstOrDefault<T>(this MyQueryable<T> source)
{
source.TakeValue = 1;
return source.AsEnumerable().FirstOrDefault();
}
public static T First<T>(this MyQueryable<T> source)
{
source.TakeValue = 1;
return source.AsEnumerable().First();
}
public static MyQueryable<T> ToMyQueryable<T>(this IEnumerable<T> source)
{
var value = source as MyQueryable<T>;
if (value == null)
value = new MyQueryable<T>(source);
return value;
}
}
public class MyQueryable<T> : IEnumerable<T>
{
public MyQueryable()
{
}
public MyQueryable(IEnumerable<T> enumerable)
{
Enumerable = enumerable;
}
public IEnumerable<T> Enumerable { get; set; }
public IEnumerator<T> GetEnumerator()
{
return Enumerable.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
private bool _allEntities = true;
public bool AllEntities
{
get { return _allEntities; }
internal set
{
_allEntities = value;
if (value)
{
IncludeValues.Clear();
WhereValue = null;
OrderByValue = null;
SkipValue = null;
TakeValue = null;
}
}
}
private ObservableCollection<string> _includeValues;
public ObservableCollection<string> IncludeValues
{
get
{
if (_includeValues == null)
{
_includeValues = new ObservableCollection<string>();
_includeValues.CollectionChanged += (sender, e) =>
{
if (e.Action == NotifyCollectionChangedAction.Add)
AllEntities = false;
};
}
return _includeValues;
}
}
private string _whereValue;
public string WhereValue
{
get { return _whereValue; }
internal set
{
if (value != null)
AllEntities = false;
_whereValue = value;
}
}
private string _orderByValue;
public string OrderByValue
{
get { return _orderByValue; }
internal set
{
if (value != null)
AllEntities = false;
_orderByValue = value;
}
}
private int? _skipValue;
public int? SkipValue
{
get { return _skipValue; }
internal set
{
if (value.HasValue)
AllEntities = false;
_skipValue = value;
}
}
private int? _takeValue;
public int? TakeValue
{
get { return _takeValue; }
internal set
{
if (value.HasValue)
AllEntities = false;
_takeValue = value;
}
}
}
Cela vous paraît peut-être bizarre pour l’instant mais attendez de voir la suite.
Ensuite, je me suis créé un projet de Test. Dans ce projet, j’ajoute la référence vers mon service wcf en incluant la référence vers mon projet Entities et celle vers Client.LINQ. Pour pouvoir utiliser mon “faux” provider LINQ, il me faut des MyQueryable de mes types d’entités. Là-aussi, (j’espère que vous l’aviez deviné), template T4.
public partial class NorthwindClientContext
{
private INorthwindService _service;
public NorthwindClientContext(INorthwindService service)
{
_service = service;
}
public MyQueryable<Customer> Customers
{
get
{
var value = new MyQueryable<Customer>();
value.Enumerable = GetCustomers(value);
return value;
}
}
private IEnumerable<Customer> GetCustomers(MyQueryable<Customer> myQueryable)
{
IEnumerable<Customer> value;
if (myQueryable.AllEntities)
value = _service.GetAllCustomers();
else
value = _service.GetCustomers(myQueryable.IncludeValues.ToList(), myQueryable.WhereValue, myQueryable.OrderByValue, myQueryable.SkipValue, myQueryable.TakeValue);
foreach (var entity in value)
yield return entity;
}
public MyQueryable<Order> Orders
{
get
{
var value = new MyQueryable<Order>();
value.Enumerable = GetOrders(value);
return value;
}
}
private IEnumerable<Order> GetOrders(MyQueryable<Order> myQueryable)
{
IEnumerable<Order> value;
if (myQueryable.AllEntities)
value = _service.GetAllOrders();
else
value = _service.GetOrders(myQueryable.IncludeValues.ToList(), myQueryable.WhereValue, myQueryable.OrderByValue, myQueryable.SkipValue, myQueryable.TakeValue);
foreach (var entity in value)
yield return entity;
}
public MyQueryable<OrderDetail> OrderDetails
{
get
{
var value = new MyQueryable<OrderDetail>();
value.Enumerable = GetOrderDetails(value);
return value;
}
}
private IEnumerable<OrderDetail> GetOrderDetails(MyQueryable<OrderDetail> myQueryable)
{
IEnumerable<OrderDetail> value;
if (myQueryable.AllEntities)
value = _service.GetAllOrderDetails();
else
value = _service.GetOrderDetails(myQueryable.IncludeValues.ToList(), myQueryable.WhereValue, myQueryable.OrderByValue, myQueryable.SkipValue, myQueryable.TakeValue);
foreach (var entity in value)
yield return entity;
}
}
L’utilisation du yield return me permet une exécution différé et m’assure que les propriétés de MyQueryable sont correctement renseignées lorsque la méthode GetCustomers / GetOrders / GetOrderDetails est appelée.
Ainsi, le code suivant :
var order = (from o in new NorthwindClientContext(service).Orders.Include("Customer").Include("OrderDetails")
where o.ShipCity == "PARIS"
orderby o.OrderDate
select o).Skip(2).First();
génèrera un appel à la méthode
_service.GetOrders(myQueryable.IncludeValues.ToList(), myQueryable.WhereValue, myQueryable.OrderByValue, myQueryable.SkipValue, myQueryable.TakeValue)
avec les paramètres suivants :
Ce qui génèrera la requête SQL suivante incluant tout cela :
SELECT
[Project1].[OrderID] AS [OrderID],
[Project1].[CustomerID] AS [CustomerID],
[Project1].[EmployeeID] AS [EmployeeID],
[Project1].[OrderDate] AS [OrderDate],
[Project1].[RequiredDate] AS [RequiredDate],
[Project1].[ShippedDate] AS [ShippedDate],
[Project1].[ShipVia] AS [ShipVia],
[Project1].[Freight] AS [Freight],
[Project1].[ShipName] AS [ShipName],
[Project1].[ShipAddress] AS [ShipAddress],
[Project1].[ShipCity] AS [ShipCity],
[Project1].[ShipRegion] AS [ShipRegion],
[Project1].[ShipPostalCode] AS [ShipPostalCode],
[Project1].[ShipCountry] AS [ShipCountry],
[Project1].[CustomerID1] AS [CustomerID1],
[Project1].[CompanyName] AS [CompanyName],
[Project1].[ContactName] AS [ContactName],
[Project1].[ContactTitle] AS [ContactTitle],
[Project1].[Address] AS [Address],
[Project1].[City] AS [City],
[Project1].[Region] AS [Region],
[Project1].[PostalCode] AS [PostalCode],
[Project1].[Country] AS [Country],
[Project1].[Phone] AS [Phone],
[Project1].[Fax] AS [Fax],
[Project1].[C1] AS [C1],
[Project1].[OrderID1] AS [OrderID1],
[Project1].[ProductID] AS [ProductID],
[Project1].[UnitPrice] AS [UnitPrice],
[Project1].[Quantity] AS [Quantity],
[Project1].[Discount] AS [Discount]
FROM ( SELECT
[Limit1].[OrderID] AS [OrderID],
[Limit1].[CustomerID1] AS [CustomerID],
[Limit1].[EmployeeID] AS [EmployeeID],
[Limit1].[OrderDate] AS [OrderDate],
[Limit1].[RequiredDate] AS [RequiredDate],
[Limit1].[ShippedDate] AS [ShippedDate],
[Limit1].[ShipVia] AS [ShipVia],
[Limit1].[Freight] AS [Freight],
[Limit1].[ShipName] AS [ShipName],
[Limit1].[ShipAddress] AS [ShipAddress],
[Limit1].[ShipCity] AS [ShipCity],
[Limit1].[ShipRegion] AS [ShipRegion],
[Limit1].[ShipPostalCode] AS [ShipPostalCode],
[Limit1].[ShipCountry] AS [ShipCountry],
[Limit1].[CustomerID2] AS [CustomerID1],
[Limit1].[CompanyName] AS [CompanyName],
[Limit1].[ContactName] AS [ContactName],
[Limit1].[ContactTitle] AS [ContactTitle],
[Limit1].[Address] AS [Address],
[Limit1].[City] AS [City],
[Limit1].[Region] AS [Region],
[Limit1].[PostalCode] AS [PostalCode],
[Limit1].[Country] AS [Country],
[Limit1].[Phone] AS [Phone],
[Limit1].[Fax] AS [Fax],
[Extent3].[OrderID] AS [OrderID1],
[Extent3].[ProductID] AS [ProductID],
[Extent3].[UnitPrice] AS [UnitPrice],
[Extent3].[Quantity] AS [Quantity],
[Extent3].[Discount] AS [Discount],
CASE WHEN ([Extent3].[OrderID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
FROM (SELECT TOP (1) [Filter1].[OrderID], [Filter1].[CustomerID1], [Filter1].[EmployeeID], [Filter1].[OrderDate], [Filter1].[RequiredDate], [Filter1].[ShippedDate], [Filter1].[ShipVia], [Filter1].[Freight], [Filter1].[ShipName], [Filter1].[ShipAddress], [Filter1].[ShipCity], [Filter1].[ShipRegion], [Filter1].[ShipPostalCode], [Filter1].[ShipCountry], [Filter1].[CustomerID2], [Filter1].[CompanyName], [Filter1].[ContactName], [Filter1].[ContactTitle], [Filter1].[Address], [Filter1].[City], [Filter1].[Region], [Filter1].[PostalCode], [Filter1].[Country], [Filter1].[Phone], [Filter1].[Fax]
FROM ( SELECT [Extent1].[OrderID] AS [OrderID], [Extent1].[CustomerID] AS [CustomerID1], [Extent1].[EmployeeID] AS [EmployeeID], [Extent1].[OrderDate] AS [OrderDate], [Extent1].[RequiredDate] AS [RequiredDate], [Extent1].[ShippedDate] AS [ShippedDate], [Extent1].[ShipVia] AS [ShipVia], [Extent1].[Freight] AS [Freight], [Extent1].[ShipName] AS [ShipName], [Extent1].[ShipAddress] AS [ShipAddress], [Extent1].[ShipCity] AS [ShipCity], [Extent1].[ShipRegion] AS [ShipRegion], [Extent1].[ShipPostalCode] AS [ShipPostalCode], [Extent1].[ShipCountry] AS [ShipCountry], [Extent2].[CustomerID] AS [CustomerID2], [Extent2].[CompanyName] AS [CompanyName], [Extent2].[ContactName] AS [ContactName], [Extent2].[ContactTitle] AS [ContactTitle], [Extent2].[Address] AS [Address], [Extent2].[City] AS [City], [Extent2].[Region] AS [Region], [Extent2].[PostalCode] AS [PostalCode], [Extent2].[Country] AS [Country], [Extent2].[Phone] AS [Phone], [Extent2].[Fax] AS [Fax], row_number() OVER (ORDER BY [Extent1].[OrderDate] ASC) AS [row_number]
FROM [dbo].[Orders] AS [Extent1]
LEFT OUTER JOIN [dbo].[Customers] AS [Extent2] ON [Extent1].[CustomerID] = [Extent2].[CustomerID]
WHERE [Extent1].[ShipCity] = 'PARIS'
) AS [Filter1]
WHERE [Filter1].[row_number] > 2
ORDER BY [Filter1].[OrderDate] ASC ) AS [Limit1]
LEFT OUTER JOIN [dbo].[Order Details] AS [Extent3] ON [Limit1].[OrderID] = [Extent3].[OrderID]
) AS [Project1]
ORDER BY [Project1].[OrderDate] ASC, [Project1].[OrderID] ASC, [Project1].[CustomerID1] ASC, [Project1].[C1] ASC
Si on enlève les Include, la requête SQL est tout de suite plus lisible
:
SELECT TOP (1)
[Filter1].[OrderID] AS [OrderID],
[Filter1].[CustomerID] AS [CustomerID],
[Filter1].[EmployeeID] AS [EmployeeID],
[Filter1].[OrderDate] AS [OrderDate],
[Filter1].[RequiredDate] AS [RequiredDate],
[Filter1].[ShippedDate] AS [ShippedDate],
[Filter1].[ShipVia] AS [ShipVia],
[Filter1].[Freight] AS [Freight],
[Filter1].[ShipName] AS [ShipName],
[Filter1].[ShipAddress] AS [ShipAddress],
[Filter1].[ShipCity] AS [ShipCity],
[Filter1].[ShipRegion] AS [ShipRegion],
[Filter1].[ShipPostalCode] AS [ShipPostalCode],
[Filter1].[ShipCountry] AS [ShipCountry]
FROM ( SELECT [Extent1].[OrderID] AS [OrderID], [Extent1].[CustomerID] AS [CustomerID], [Extent1].[EmployeeID] AS [EmployeeID], [Extent1].[OrderDate] AS [OrderDate], [Extent1].[RequiredDate] AS [RequiredDate], [Extent1].[ShippedDate] AS [ShippedDate], [Extent1].[ShipVia] AS [ShipVia], [Extent1].[Freight] AS [Freight], [Extent1].[ShipName] AS [ShipName], [Extent1].[ShipAddress] AS [ShipAddress], [Extent1].[ShipCity] AS [ShipCity], [Extent1].[ShipRegion] AS [ShipRegion], [Extent1].[ShipPostalCode] AS [ShipPostalCode], [Extent1].[ShipCountry] AS [ShipCountry], row_number() OVER (ORDER BY [Extent1].[OrderDate] ASC) AS [row_number]
FROM [dbo].[Orders] AS [Extent1]
WHERE [Extent1].[ShipCity] = 'PARIS'
) AS [Filter1]
WHERE [Filter1].[row_number] > 2
ORDER BY [Filter1].[OrderDate] ASC
On retrouve bien notre WHERE City = ‘Paris’, notre WHERE row_number > 2 (pour le skip), notre ORDER BY OrderDate et notre TOP 1 (pour le First).
Attention, ce provider LINQ est un POC. Il manque beaucoup de choses (pas forcément très difficile à rajouter d’ailleurs) tel que l’utilisation des variables et des new (pour comparer par rapport à une date par ex, etc.)
Que se passe-t-il si vous intégrez des méthodes non supportées dans ClientLINQ ?
Ca se passe très bien ! 
En effet, le résultat de votre méthode non supportée ne sera pas un MyQueryable. Par conséquent, ce sont les méthodes de LINQ To Object qui seront utilisées.
Par exemple, la requête suivante :
var customerInfos = (from o in new NorthwindClientContext(service).Orders.Include("Customer")
where o.ShipCity == "PARIS"
orderby o.OrderDate
group o by o.Customer into g
select new { g.Key.CompanyName, g.Key.ContactName, OrdersCount = g.Count() }).ToList();
génèrera un appel à la méthode
_service.GetOrders(myQueryable.IncludeValues.ToList(), myQueryable.WhereValue, myQueryable.OrderByValue, myQueryable.SkipValue, myQueryable.TakeValue)
avec les paramètres suivants :
ce qui génèrera la requête SQL suivante :
SELECT
[Extent1].[OrderID] AS [OrderID],
[Extent1].[CustomerID] AS [CustomerID],
[Extent1].[EmployeeID] AS [EmployeeID],
[Extent1].[OrderDate] AS [OrderDate],
[Extent1].[RequiredDate] AS [RequiredDate],
[Extent1].[ShippedDate] AS [ShippedDate],
[Extent1].[ShipVia] AS [ShipVia],
[Extent1].[Freight] AS [Freight],
[Extent1].[ShipName] AS [ShipName],
[Extent1].[ShipAddress] AS [ShipAddress],
[Extent1].[ShipCity] AS [ShipCity],
[Extent1].[ShipRegion] AS [ShipRegion],
[Extent1].[ShipPostalCode] AS [ShipPostalCode],
[Extent1].[ShipCountry] AS [ShipCountry],
[Extent2].[CustomerID] AS [CustomerID1],
[Extent2].[CompanyName] AS [CompanyName],
[Extent2].[ContactName] AS [ContactName],
[Extent2].[ContactTitle] AS [ContactTitle],
[Extent2].[Address] AS [Address],
[Extent2].[City] AS [City],
[Extent2].[Region] AS [Region],
[Extent2].[PostalCode] AS [PostalCode],
[Extent2].[Country] AS [Country],
[Extent2].[Phone] AS [Phone],
[Extent2].[Fax] AS [Fax]
FROM [dbo].[Orders] AS [Extent1]
LEFT OUTER JOIN [dbo].[Customers] AS [Extent2] ON [Extent1].[CustomerID] = [Extent2].[CustomerID]
WHERE [Extent1].[ShipCity] = 'PARIS'
ORDER BY [Extent1].[OrderDate] ASC
Comme vous pouvez le constater, pas de trace de GROUP BY. Pourtant le résultat a bien pris en compte mon group by (en LINQ To Object).
Le but de ce post était certes de présenter un “faux” provider LINQ rigolo en se basant sur le fait que le yield return diffère l’exécution de la méthode mais surtout de vous convaincre (et je suis sûr que vous l’êtes
) par le gain de productivité du couple EF / T4. En effet,
- Vos templates sont réutilisables de projet en projet. Il suffit de changer le path de l’edmx et de dire à Visual Studio de regénérer le code de tous les templates T4.
- Si vous ne les avez pas déjà écrit, le temps de dev n’est pas proportionnel au nombre d’entités ce qui implique un gain de productivité très rapide par rapport à un développement classique.
Les classes / interfaces générées par T4 peuvent être partial. Il sera donc très simple de les compléter pour rajouter une partie custom.
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :