LINQ to SQL and WCF - Sharing types, subverting the DataContext on the client side(转)
Posted on 2007-10-15 21:09 江南白衣 阅读(1760) 评论(3) 编辑 收藏 举报原文:http://mtaulty.com/CommunityServer/blogs/mike_taultys_blog/archive/2007/10/09/9848.aspx
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;
- 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.
- 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.
- 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>