My interests are in: - Microsoft Visual Studio .Net - Microsoft SQL Server. - Delphi. - ERP. - DNS, SOA, SAS, ... etc.

Friday, March 7, 2008

LINQ to SQL and WCF - Sharing types, subverting the DataContext on the client side

I've found a starting ideas in this very helpfull chainais blog. He wrote about creating a DataContext in the client side with an empty connection string. This DataContext will be used in the first fitch of the data or after savings to keep the original entities. Then, before saves, he will compare the modified entitites witht the original one.
The original post is here.

Ok, so this whole post probably falls into the category of "nasty hack" but
it was something that I was playing with so I thought I'd share.


    I wrote a little

bit here previously
about working with LINQ to SQL in a disconnected sense
and wrote about how, if you wanted to manage concurrency in a reasonable way,
then you needed to be able to pass back to a middle-tier server the state of any
entities that you were deleting or updating in order that the server could see
which properties you'd actually updated and which (if any) of the set of
properties that are significant for optimistic concurrency might have been
changed by someone else since you took your data from the database.


    Now, in transferring data to a client from that
middle-tier server it seems to me that you can either;



  1. Share contract with the client. This is the
    "web services" way and divorces anything that happens on the client from
    what happens in the server. It has advantages (e.g. interoperability,
    versioning) but it's also quite hard to do because none of the clever bits
    that you have on the server side is available to you on the client side.

  2. Share types with the client. This is simpler
    to program against but might hurt you from the versioning point of view and
    almost certainly doesn't help you do interop.

    1. Note - the approach of sharing a DataSet between
      the client and the server falls into this category with the difference
      being that (for .NET) you're only sharing a single data type (i.e.
      DataSet) between the client and the
      server and that type isn't actually owned by you so maybe it's not so
      bad.




Whichever of these you go for, if you're returning objects that you get from
LINQ to SQL then you don't really have the DataSet (and its
ability to track changes) to help you and that means that someone, somewhere
(and a good candidate is the client) has to store "before" and "after" values
for the entities that you're manipulating on the client.


From here on in, I'm only talking about the shared types
approach.


If we were to go with the shared types approach
then it becomes interesting because (given the right flags) the LINQ to SQL bits
will give you data types (e.g. Customer, Order and so on from a
Northwind DB) that you can serialize straight to a client over WCF with little
effort.


So, down on the client side you've got a bunch of Customer
instances and these types are quite smart in that they already implement
INotifyPropertyChang[ing/ed]
and so can tell an interested party about
changes to themselves and this might well be a useful building block for you to
be able to build something client-side which allows you to relatively easily
determine which objects have been inserted, updated, deleted when it comes time
to submit them back to a middle-tier service.


I started to think about building some class that would sync up to these
property changed notifications for use on the client side and I kept coming back
to thinking "Hang, on - the DataContext can already do all this
stuff".


So...is it possible to make use of the DataContext client
side in order to do this "change notification stuff" ? It appears that it might
well be but this is where it all gets a bit hacky.


Here's where I ended up.


1) Built a class library project called DataTypes


Into this class library project I added the output of running
sqlmetal.exe /server:. /database:northwind /serialization:Unidirectional
/pluralize /code:northwind.cs
and that means that I've now got one
project with the data types that I need to serialize backwards and forwards to
my service code all contained in one place so that I can reference them from my
client and my service (shared types!).


2) Build a class library project called ServiceInterface


Into this, I just added the following WCF marked up interface;


namespace ServiceInterface
{
[ServiceContract]
public interface IServeCustomers
{
[OperationContract]
List<Customer> GetCustomersForCountry(string country);
[OperationContract]
void InsertCustomers(List<Customer> customers);
[OperationContract]
void DeleteCustomers(List<Customer> before,
List<Customer> after);
[OperationContract]
void UpdateCustomers(List<Customer> before,
List<Customer> after);
}
}

This references the DataTypes project and, again, can itself
be referenced by both my client and my service.


3) Built a WCF service to handle Customer instances
(console app)


This is very similar to what I did in a previous post. The service code looks
like this and references the ServiceInterface and
DataTypes
projects;


namespace Service
{
class Implementation : IServeCustomers
{
public List<Customer> GetCustomersForCountry(string country)
{
List<Customer> customers = null;
using (NorthwindDataContext ctx = new NorthwindDataContext())
{
ctx.ObjectTrackingEnabled = false;
customers =
(from c in ctx.Customers where c.Country == country select c).ToList();
}
return (customers);
}
public void InsertCustomers(List<Customer> customers)
{
using (NorthwindDataContext ctx = new NorthwindDataContext())
{
foreach (Customer c in customers)
{
ctx.Customers.Add(c);
}
ctx.SubmitChanges();
}
}
public void DeleteCustomers(List<Customer> before,
List<Customer> after)
{
using (NorthwindDataContext ctx = new NorthwindDataContext())
{
for (int i = 0; i < before.Count; i++)
{
ctx.Customers.Attach(after[i], before[i]);
ctx.Customers.Remove(after[i]);
}
ctx.SubmitChanges();
}
}
public void UpdateCustomers(List<Customer> before, List<Customer> after)
{
using (NorthwindDataContext ctx = new NorthwindDataContext())
{
for (int i = 0; i < before.Count; i++)
{
ctx.Customers.Attach(after[i], before[i]);
}
ctx.SubmitChanges();
}
}
}
}

 


And then I've got the hosting code;


    static void Main(string[] args)
{
ServiceHost host = new ServiceHost(typeof(Implementation));
host.Open();
Console.WriteLine("Listening...");
Console.ReadLine();
host.Close();
}

 


and the config file;


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="DataTypes.Properties.Settings.NorthwindConnectionString"
connectionString="Data Source=.;Initial Catalog=Northwind;Integrated Security=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
<system.serviceModel>
<services>
<service name="Service.Implementation">
<endpoint address="net.tcp://localhost:9091/customerService"
binding="netTcpBinding"
contract="ServiceInterface.IServeCustomers"
bindingConfiguration="myConfig"/>
</service>
</services>
<bindings>
<netTcpBinding>
<binding name="myConfig">
<security mode="None"/>
</binding>
</netTcpBinding>
</bindings>
</system.serviceModel>
</configuration>

 


4) Built a client (console app)


This project references both the ServiceInterface and
DataTypes project. I hand-cranked a WCF proxy class as I
couldn't remember the tool option to do it;


  class ClientProxy : ClientBase<IServeCustomers>, IServeCustomers
{
public ClientProxy()
{
}
public ClientProxy(string config) : base(config)
{
}
public List<Customer> GetCustomersForCountry(string country)
{
return (base.Channel.GetCustomersForCountry(country));
}
public void InsertCustomers(List<Customer> customers)
{
base.Channel.InsertCustomers(customers);
}
public void DeleteCustomers(List<Customer> before,
List<Customer> after)
{
base.Channel.DeleteCustomers(before, after);
}
public void UpdateCustomers(List<Customer> before,
List<Customer> after)
{
base.Channel.UpdateCustomers(before, after);
}
}

 


and then I wrote this little class to make use of the underlying
DataContext
on the client side. Note - this is really a bit
subversive and possibly a bit evil but I thought I'd have a play with it.
Essentially, I'm trying to use the change-tracking capabilities of a DataContext
without ever trying to connect it to a DB which is not really what you're meant
to do (AFAIK). Anyway...


  public class ClientSideContext : IDisposable
{
public class StateEntries<T>
{
public List<T> Originals { get; set; }
public List<T> Current { get; set; }
}
public ClientSideContext()
{
ctx = new DataContext("", new AttributeMappingSource());
ctx.DeferredLoadingEnabled = false;
}
public void Attach<T>(T t) where T : class
{
ctx.GetTable<T>().Attach(t);
}
public void Remove<T>(T t) where T : class
{
ctx.GetTable<T>().Remove(t);
}
public void Add<T>(T t) where T : class
{
ctx.GetTable<T>().Add(t);
}
public List<T> GetInserted<T>() where T : class
{
return (GetChangeEntries<T>(ch => ch.AddedEntities));
}
public StateEntries<T> GetDeleted<T>() where T : class
{
return (GetStateEntries<T>(ch => ch.RemovedEntities));
}
private StateEntries<T> GetStateEntries<T>(
Func<ChangeSet, IEnumerable<Object>> entry) where T : class
{
List<T> current = GetChangeEntries<T>(entry);
List<T> originals = GetOriginals<T>(current);
return (new StateEntries<T>()
{
Originals = originals,
Current = current
});
}
public StateEntries<T> GetModified<T>() where T : class
{
return (GetStateEntries<T>(ch => ch.ModifiedEntities));
}
public void Dispose()
{
ctx.Dispose();
}
List<T> GetChangeEntries<T>(
Func<ChangeSet, IEnumerable<Object>> selectMember) where T : class
{
var query = from o in selectMember(ctx.GetChangeSet())
where ((o as T) != null)
select (T)o;
return (new List<T>(query));
}
List<T> GetOriginals<T>(List<T> current) where T : class
{
List<T> originals = new List<T>(
from c in current
select ctx.GetTable<T>().GetOriginalEntityState(c));
return (originals);
}
private DataContext ctx;
}

 


So, the idea here is that this class contains a DataContext
and allows you to Attach, Add, Remove instances to it. You can
then come back at a later point (presumably when you want to call back to your
middle-tier service) and you can do GetInserted(), GetModified(),
GetDeleted()
and it'll feed you the lists of objects (including
original values and current values where necessary) to pass back to that
middle-tier service.


Here's the client program code that I was playing with to try and exercise
this;


 


   static void Main(string[] args)
{
Console.WriteLine("Hit return to make call...");
Console.ReadLine();
ClientProxy proxy = new ClientProxy("clientConfig");
List<Customer> customers = proxy.GetCustomersForCountry("UK");
proxy.Close();
using (ClientSideContext ctx = new ClientSideContext())
{
foreach (Customer c in customers)
{
ctx.Attach(c);
// Simulate an update...
c.Country = "GB";
}
// Now insert...
ctx.Add(new Customer()
{
CustomerID = "Foo",
CompanyName = "Bar"
});
// Now delete...
ctx.Remove(customers[0]);
// Now, call back to service...
proxy = new ClientProxy("clientConfig");
proxy.InsertCustomers(ctx.GetInserted<Customer>());
var deleted = ctx.GetDeleted<Customer>();
proxy.DeleteCustomers(deleted.Originals, deleted.Current);
var modified = ctx.GetModified<Customer>();
proxy.UpdateCustomers(modified.Originals, modified.Current);
proxy.Close();
Console.ReadLine();
}
}

Naturally, this is hacky and perhaps it would have been better to write my
own class on the client-side rather than trying to bend the
DataContext
to do something like this but it seemed easier for what I
was playing with so I gave it a whirl and thought I'd share.


( No doubt, there are places where it'll go wrong :-) ).


Here's the config file for the client, just for completeness;


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<client>
<endpoint name="clientConfig"
address="net.tcp://localhost:9091/customerService"
binding="netTcpBinding"
contract="ServiceInterface.IServeCustomers"
bindingConfiguration="myConfig"/>
</client>
<bindings>
<netTcpBinding>
<binding name="myConfig">
<security mode="None"/>
</binding>
</netTcpBinding>
</bindings>
</system.serviceModel>
</configuration>

No comments: