江南白衣

陌上發花,可以緩緩醉矣
忍把浮名,換了淺斟低唱
我不是聖賢豪士,我衹有一腔熱血
posts - 113, comments - 422, trackbacks - 14, articles - 0
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理

原文: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;

  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>

Feedback

#1楼    回复  引用    

2008-03-10 01:38 by Tariq Salah [未注册用户]
I googled every where to find a way to track the changes in the client. I found only you solution lsql and the EntityBag for EF. May be it is the easiest way to track the changes in the client after the entities have been fetched.

When I try to use this scenario in a real Windows Forms Binding Source, I failed because I do no know how to query in the Local context with an iqueryable and let the local context to track the changes for me in windows forms senario?

#2楼 [楼主]   回复  引用  查看    

2008-03-12 00:08 by 江南白衣      
@Tariq Salah
hi,Tarip,this post is reprinted from http://mtaulty.com/CommunityServer/blogs/mike_taultys_blog/archive/2007/10/09/9848.aspx

"原文:http://mtaulty.com/CommunityServer/blogs/mike_taultys_blog/archive/2007/10/09/9848.aspx"

"原文" == original post:)

"LINQ to SQL and WCF - Sharing types, subverting the DataContext on the client side(转)"

"转" == Reprint:)
for your question:

public class EntityBindingList<TEntity> : BindingList<TEntity> where TEntity : class
{

private ClientSideContext context;
public ClientSideContext Context
{
get { return context; }
}

public EntityBindingList(IList<TEntity> entities)
: base(entities)
{
context = new ClientSideContext();
context.AttachAll<TEntity>(entities);
}

protected override void InsertItem(int index, TEntity item)
{
base.InsertItem(index, item);
Context.Add<TEntity>(item);
}

protected override void RemoveItem(int index)
{
Context.Remove<TEntity>(this[index]);
base.RemoveItem(index);
}
}

public class ClientSideContext
{
private DataContext ctx;

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 AttachAll<T>(IEnumerable<T> entities) where T : class
{
ctx.GetTable<T>().AttachAll(entities);
}

public void Remove<T>(T t) where T : class
{
ctx.GetTable<T>().DeleteOnSubmit(t);
}

public void Add<T>(T t) where T : class
{
ctx.GetTable<T>().InsertOnSubmit(t);
}

public List<T> GetInserted<T>() where T : class
{
return (GetChangeEntries<T>(ch => ch.Inserts));
}

public StateEntries<T> GetDeleted<T>() where T : class
{
return (GetStateEntries<T>(ch => ch.Deletes));
}

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.Updates));
}

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);
}

public void Dispose()
{
ctx.Dispose();
}
}

#3楼    回复  引用    

2008-03-15 15:36 by Tariq Salah [未注册用户]
Thank you for your help. Actually, I try to fit Linq to a very small project that is only one form with only one relational table to be connected via HTTP. It is a good chance to taste Linq. heh.
The source code becomes like this:
private EntityBindingList<TheEntity> listOfEntities;
private void InitializeEntities()
{
ServiceReferenceLinq.ServiceLinqClient svc = new ServiceLinqClient();
// TheBindingSource.DataSource = new EntityBindingList<PurchPOMst>(svc.GetPurchPoMst());


listOfEntities = new EntityBindingList<TheEntity>(svc.GetPurchPoMst());
TheBindingSource.DataSource = listOfEntities;
svc.Close();
}
I can fetch the entities but when I try to add new entity using the BindnigSource. it is giving me a “collection is read only”.

标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      


相关链接: