Data oriented WCF service Agile with EF4 and a “sort of” provider LINQ
I use a different approach than Julie’s one for EF Agile development. Indeed, I use here Self-Tracking Entities and I massively use T4.
The first step is to create a project DAL in which we will add an edmx.
In my sample, I use Northwind DataBase with only the Customers, Orders and [Order Details] tables.
When it’s done, we will add a new item of type ADO.NET Self-Tracking Entity Generator to our project.
It will add two T4 templates to our project:
- one with entities + some classes and interfaces for Self-Tracking
- one with the context + a static class with extension methods.
We will move our entities template into another project Entities.
Then we have to add the Entities reference in our DAL project. We will also set the edmx path in our templates.
Then, we will create a new project Repositories. In this project, we will define an interface INorthwindRepository.
We can see the redundancy of INorthwindRepository. That’s why, in my sample, these interfaces (INorthwindRepository and IEntitySet) are also generated with a T4 template which uses my edmx. What is great here is the fact that if we change our data model (my edmx), we have no more code to write. We just have to regenerate all my T4 templates.
Now, we will go back to my context. It has to implement INorthwindRepository. For this, we will update my T4 template to have this:
For WCF, we need a contract. So I will add two new projects: Services and ServiceContracts (with T4).
So now, if you already have the T4 templates (which are independent of the project), you wrote 0 lines of code!
We can see the assemblies independence with EF (except for the DAL of course) :
I wanted to use Unity. To do it, I use Alexey Zakharov approach. So I integrate his WCFFacility project and classes Bootstrapper and UnityServiceLocatorAdapter in my WCFService (web project host). After creating the svc and the config file, my service is finished!
Now in the client tier, I wanted to use a “sort of” provider LINQ. For this, I inspired from one of my old demo made for ALT.NET French community.
We will create a Client.LINQ project in which we will define two classes: ClientLINQ and 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;
}
}
} Then, we will create our client project (a Test project in my sample). In this project, I add Entities project and Client.LINQ references. Client.LINQ is not mandatory. I can also duplicate my entities code but in order to have Self-Tracking, I can’t use the default generated code. Then, we will add the WCF service reference. To use my “sort of” provider LINQ, I need some MyQueryable of my entities. Here also, (I hope that you guess it), we can use a T4 template.
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;
}
} The use of the yield return will postpone the execution and allow MyQueryable properties to be set when GetCustomers / GetOrders / GetOrderDetails methods are called.
So the following code:
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();
will call this method
_service.GetOrders(myQueryable.IncludeValues.ToList(), myQueryable.WhereValue, myQueryable.OrderByValue, myQueryable.SkipValue, myQueryable.TakeValue)
with these parameters:
wich will generate the following SQL query:
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 If we comment the Include, the SQL query is more readable
:
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 We can find our WHERE City = ‘Paris’, our WHERE row_number > 2 (for the Skip), out ORDER BY OrderDate and our TOP 1 (for the First).
Take care, this “sort of” provider LINQ is a POC. It is not finished. For example, it doesn’t support variables or the new operator (for date for example).
What happens if we integrate methods non supported by ClientLINQ?
It’s ok! 
LINQ To Object will be used.
For example, the following LINQ query:
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();
will call this method
_service.GetOrders(myQueryable.IncludeValues.ToList(), myQueryable.WhereValue, myQueryable.OrderByValue, myQueryable.SkipValue, myQueryable.TakeValue)
with these parameters:
which will generate the following SQL query:
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
As you can see, no trace of group by. However, the result integrates it (with LINQ To Object).
In this post, I wanted to present my “sort of'” LINQ provider but the real goal of this post is to persuade you (and I’m sure you are
) of the EF / T4 couple productivity gain. Indeed,
- your templates are reusable in different projects. You just have to update the edmx path and to let Visual Studio regenerate T4 templates code.
- If you haven't already written the template you need, the dev time is not proportional to the number of entities which implies very quickly a productivity gain compare to a “classic” dev.