Full-graph reattachment of disconnected LINQ entities (or "Is MS just trying to piss me off?")

by Jared 3. September 2008 21:11

LINQ...

Language INtegrated Query...

It's the first thing I wrote about in this blog.  I'm completely sold on the importance of this technology, and I still assert it will have a serious impact on many, many applications written for the .Net Framework, but the shipping product is just not complete, and it's incomplete in some really annoying ways.

I've recently been working with LINQ-to-SQL (aka L2S) trying to develop some kind of comparison for myself between the L2S and nHibernate.  I understand the current community affection for nHibernate, but there are a number of factors that make me want to explore alternatives - if for no other reason than to convince myself that choosing nHibernate is appropriate.

On the surface, L2S looks great.  The "out-of-the-box" experience is slick and intuitive, like most MS demo-ware.  But the issues start to show when you look more closely:

1.  Support for Plain Old CLR Objects (POCOs) is available, but is all but hidden under all of the marketing material.  Usually there's a good deal of pressure to use the drag and drop tools, but I found it nearly impossible to locate decent articles from MS about using undecorated (un-autogenerated, for that matter) types.

Example:

001using System.Linq;
002using System.Web;
003using System.Web.Security;
004using System.Web.UI;
005using System.Web.UI.HtmlControls;
006using System.Web.UI.WebControls;
007using System.Web.UI.WebControls.WebParts;
008using System.Xml.Linq;
009
010using System.ComponentModel;
011
012namespace LINQSamples
013{
014    [Serializable]
015    public class Product
016    {
017        #region Fields
018
019        private int _productId;
020        private int _categoryId;
021        private string _name;
022        private decimal _unitPrice;
023        private Category _category;
024
025        #endregion Fields
026
027        #region Properties
028
029        public int ProductId
030        {
031            get { return _productId; }
032            set { _productId = value; }
033        }
034
035        public int CategoryId
036        {
037            get { return _categoryId; }
038            set 
039            {
040                _categoryId = value;
041            }
042        }
043
044        public string Name
045        {
046            get { return _name; }
047            set 
048            {
049                _name = value;
050           }
051        }
052
053        public decimal UnitPrice
054        {
055            get { return _unitPrice; }
056            set 
057            {
058                _unitPrice = value;
059            }
060        }
061
062        public Category Category
063        {
064            get { return _category; }
065            set 
066            {
067                 _category = value;
068             }
069        }
070
071        #endregion Properties
072
073    }
074}

As it turns out, there is nearly full support for these types of undecorated classes through the use of XML mapping files (similar to nHibernate et al).  The only clear loss is lazy instantiation, and it is possible to roll your own on that one.  Mostly, I'm miffed because it took so long to figure out that exclusive use of mapping XML was even an option.

2.  Arcane documentation (primarily in user blogs) on the disconnected use of L2S entities - specifically, how to detach them (that's easy, just dispose the DataContext Wink) and reattach them while retaining the ability to persist your data.  This one is a real poser.  There are tons of articles online about the ways people have chosen to attack this problem.  Basically, the issue is this: L2S (much like nHibernate) is intended to be a connected technology.  Microsoft would like very much to have everyone divide their entity usage up into easily digestible work units that have discrete begin- and endpoints.

* Open a DataContext
* Work with entities (enjoy LINQy goodness while you're at it)
* Submit any changes to be persisted
* Close and dispose of the DataContext

...in that order.  

As long as one follows those guidelines, L2S really does behave itself very well.  However, in the real world there are, I think, very defensible arguments for using entities that can be disconnected from their contexts and reconnected at a later time when persistence is required.  This is where things started to seriously break down.  There are plenty of articles about the weird behavior of L2S entities when they're disconnected and subsequently reconnected (try here and here and here and a couple hundred other places).  Basically, it's pretty much mass confusion, but what it boiled down to for me was the use of the System.Data.Linq.Table<T>.Attach() function coupled with the System.Data.Linq.DataContext.Refresh() function like so:

001Product product = null;
002using (SampleContext db = new SampleContext(connString, map))
003{
004    product = db.Products.Single(p => p.ProductId == 1);
005}
006
007product.Name += " this is a change";
008
009using (SampleContext db = new SampleContext(connString, map))
010{
011    db.Products.Attach(product);
012    db.Refresh(RefreshMode.KeepCurrentValues, product);
013    db.SubmitChanges();
014}

It looks simple enough, and produces reasonable SQL, but there's a significant "gotcha" - it only works for the topmost object in the graph.  In other words, even though the product in the example may have a changed Category child object, the example will only result in the product's update in the database.  Oopsy!  That was a showstopper for me.  How on earth could something like that have been overlooked?

So, lacking the sense to just stop there, I went ahead and started looking for general ways to get around this glaring limitation.  That involved first finding where those clever developers had stashed the mapping and association information for the DataContext, which took a while.  Once I found that, I was able to set up a recursive function to basically walk down a hierarchy of entity collections on an object graph, adding reattaching them as it went.  I implemented my changes as an extension method to the ITable interface, which makes it convenient to use from any Table<T> in the DataContext.  The code isn't complete, but there's enough here to get an idea where I'm going:

001using System;
002using System.Data;
003using System.Configuration;
004using System.Linq;
005using System.Xml.Linq;
006
007
008using System.Collections;
009using System.Collections.Generic;
010using System.Collections.ObjectModel;
011using System.Data.Linq;
012using System.Data.Linq.Mapping;
013
014
015using System.Reflection;
016
017
018namespace LINQSamples
019{
020    public static class LinqToSqlExtensions
021    {
022
023
024        #region Fields
025
026
027        private static Dictionary<Type, Dictionary<string, PropertyInfo>> _propertyCache;
028
029
030        #endregion Fields
031
032
033        #region Ctor
034
035
036        static LinqToSqlExtensions()
037        {
038            _propertyCache = new Dictionary<Type, Dictionary<string, PropertyInfo>>();
039        }
040
041
042        #endregion Ctor
043
044
045        #region Extension Methods
046
047
048        #region Re-attach to Table<T>
049
050
051        /// <summary>
052        /// Attaches a disconnected entity to a new <see>DataContext</see>.
053        /// </summary>
054        /// <remarks>
055        /// <para>
056        /// The default behavior, unless overridden, is to attach all members of the provided object graph to the <see>DataContext</see>, 
057        /// retaining current values.
058        /// </para>
059        /// <para>
060        /// The objective of this function is to simplify the use of LINQ to SQL in detached or semi-attached scenarios such as ASP.Net or other 
061        /// tiered architectures.
062        /// </para>
063        /// </remarks>
064        /// <typeparam name="T"></typeparam>
065        /// <param name="table">The table.</param>
066        /// <param name="obj">The obj.</param>
067        /// <param name="performReset">if set to <c>true</c> [perform reset].</param>
068        public static void ReAttach<T>(this Table<T> table, T obj, bool performRefresh) where T : class
069        {
070            if (performRefresh)
071                ReAttachWithRefresh(table, new List<T> { obj }, true, RefreshMode.KeepCurrentValues);
072            else
073                ReAttachNoRefresh(table, new List<T> { obj }, true);
074        }
075
076
077        /// <summary>
078        /// Attaches a disconnected entity to a new <see>DataContext</see>.
079        /// </summary>
080        /// <remarks>
081        /// <para>
082        /// The default behavior, unless overridden, is to attach all members of the provided object graph to the <see>DataContext</see>, 
083        /// retaining current values.
084        /// </para>
085        /// <para>
086        /// The objective of this function is to simplify the use of LINQ to SQL in detached or semi-attached scenarios such as ASP.Net or other 
087        /// tiered architectures.
088        /// </para>
089        /// </remarks>
090        /// <typeparam name="T"></typeparam>
091        /// <param name="table">The table.</param>
092        /// <param name="obj">The obj.</param>
093        public static void ReAttach<T>(this Table<T> table, T obj) where T:class
094        {
095            ReAttachWithRefresh(table, new List<T> { obj }, true, RefreshMode.KeepCurrentValues);
096        }
097
098
099        /// <summary>
100        /// Attaches a disconnected entity to a new <see>DataContext</see>.
101        /// </summary>
102        /// <remarks>
103        /// <para>
104        /// The default behavior, unless overridden, is to attach all members of the provided object graph to the <see>DataContext</see>, 
105        /// retaining current values.
106        /// </para>
107        /// <para>
108        /// The objective of this function is to simplify the use of LINQ to SQL in detached or semi-attached scenarios such as ASP.Net or other 
109        /// tiered architectures.
110        /// </para>
111        /// </remarks>
112        /// <typeparam name="T"></typeparam>
113        /// <param name="table">The table.</param>
114        /// <param name="obj">The obj.</param>
115        /// <param name="entireGraph">if set to <c>true</c> [entire graph].</param>
116        /// <param name="performRefresh">if set to <c>true</c> [perform refresh].</param>
117        public static void ReAttach<T>(this Table<T> table, T obj, bool entireGraph, bool performRefresh) where T : class
118        {
119            if (performRefresh)
120            {
121                ReAttachWithRefresh(table, new List<T> { obj }, entireGraph, RefreshMode.KeepCurrentValues);
122            }
123            else
124            {
125                ReAttachNoRefresh(table, new List<T> { obj }, entireGraph);
126            }
127        }
128
129
130        /// <summary>
131        /// Attaches a disconnected entity to a new <see>DataContext</see>.
132        /// </summary>
133        /// <remarks>
134        /// <para>
135        /// The default behavior, unless overridden, is to attach all members of the provided object graph to the <see>DataContext</see>, 
136        /// retaining current values.
137        /// </para>
138        /// <para>
139        /// The objective of this function is to simplify the use of LINQ to SQL in detached or semi-attached scenarios such as ASP.Net or other 
140        /// tiered architectures.
141        /// </para>
142        /// </remarks>
143        /// <typeparam name="T"></typeparam>
144        /// <param name="table">The table.</param>
145        /// <param name="obj">The obj.</param>
146        /// <param name="mode">The mode.</param>
147        public static void ReAttach<T>(this Table<T> table, T obj, RefreshMode mode) where T : class
148        {
149            ReAttachWithRefresh(table, new List<T> { obj }, true, mode);
150        }
151
152
153        /// <summary>
154        /// Attaches a disconnected entity to a new <see>DataContext</see>.
155        /// </summary>
156        /// <remarks>
157        /// <para>
158        /// The default behavior, unless overridden, is to attach all members of the provided object graph to the <see>DataContext</see>, 
159        /// retaining current values.
160        /// </para>
161        /// <para>
162        /// The objective of this function is to simplify the use of LINQ to SQL in detached or semi-attached scenarios such as ASP.Net or other 
163        /// tiered architectures.
164        /// </para>
165        /// </remarks>
166        /// <typeparam name="T"></typeparam>
167        /// <param name="table">The table.</param>
168        /// <param name="obj">The obj.</param>
169        /// <param name="mode">The mode.</param>
170        /// <param name="entireGraph">if set to <c>true</c> [entire graph].</param>
171        public static void ReAttach<T>(this Table<T> table, T obj, bool entireGraph, RefreshMode mode) where T : class
172        {
173            ReAttachWithRefresh(table, new List<T> { obj }, entireGraph, mode);
174        }
175
176
177        /// <summary>
178        /// Attaches a disconnected entity to a new <see>DataContext</see>.
179        /// </summary>
180        /// <remarks>
181        /// <para>
182        /// The default behavior, unless overridden, is to attach all members of the provided object graph to the <see>DataContext</see>, 
183        /// retaining current values.
184        /// </para>
185        /// <para>
186        /// The objective of this function is to simplify the use of LINQ to SQL in detached or semi-attached scenarios such as ASP.Net or other 
187        /// tiered architectures.
188        /// </para>
189        /// </remarks>
190        /// <typeparam name="T"></typeparam>
191        /// <param name="table">The table.</param>
192        /// <param name="objectSet">The object set.</param>
193        public static void ReAttach<T>(this Table<T> table, IEnumerable<T> objectSet, bool performRefresh) where T : class
194        {
195            if (performRefresh)
196                ReAttachWithRefresh(table, objectSet, true, RefreshMode.KeepCurrentValues);
197            else
198                ReAttachNoRefresh(table, objectSet, true);
199        }
200
201
202        /// <summary>
203        /// Attaches a disconnected entity to a new <see>DataContext</see>.
204        /// </summary>
205        /// <remarks>
206        /// <para>
207        /// The default behavior, unless overridden, is to attach all members of the provided object graph to the <see>DataContext</see>,
208        /// retaining current values.
209        /// </para>
210        /// <para>
211        /// The objective of this function is to simplify the use of LINQ to SQL in detached or semi-attached scenarios such as ASP.Net or other
212        /// tiered architectures.
213        /// </para>
214        /// </remarks>
215        /// <typeparam name="T"></typeparam>
216        /// <param name="table">The table.</param>
217        /// <param name="objectSet">The object set.</param>
218        public static void ReAttach<T>(this Table<T> table, IEnumerable<T> objectSet) where T : class
219        {
220            ReAttachWithRefresh(table, objectSet, true, RefreshMode.KeepCurrentValues);
221        }
222
223
224        /// <summary>
225        /// Attaches a disconnected entity to a new <see>DataContext</see>.
226        /// </summary>
227        /// <remarks>
228        /// <para>
229        /// The default behavior, unless overridden, is to attach all members of the provided object graph to the <see>DataContext</see>,
230        /// retaining current values.
231        /// </para>
232        /// <para>
233        /// The objective of this function is to simplify the use of LINQ to SQL in detached or semi-attached scenarios such as ASP.Net or other
234        /// tiered architectures.
235        /// </para>
236        /// </remarks>
237        /// <typeparam name="T"></typeparam>
238        /// <param name="table">The table.</param>
239        /// <param name="obj">The obj.</param>
240        /// <param name="entireGraph">if set to <c>true</c> [entire graph].</param>
241        /// <param name="performRefresh">if set to <c>true</c> [perform refresh].</param>
242        public static void ReAttach<T>(this Table<T> table, IEnumerable<T> objectSet, bool entireGraph, bool performRefresh) where T : class
243        {
244            if (performRefresh)
245            {
246                ReAttachWithRefresh(table, objectSet, entireGraph, RefreshMode.KeepCurrentValues);
247            }
248            else
249            {
250                ReAttachNoRefresh(table, objectSet, entireGraph);
251            }
252        }
253
254
255        /// <summary>
256        /// Attaches a disconnected entity to a new <see>DataContext</see>.
257        /// </summary>
258        /// <remarks>
259        /// <para>
260        /// The default behavior, unless overridden, is to attach all members of the provided object graph to the <see>DataContext</see>, 
261        /// retaining current values.
262        /// </para>
263        /// <para>
264        /// The objective of this function is to simplify the use of LINQ to SQL in detached or semi-attached scenarios such as ASP.Net or other 
265        /// tiered architectures.
266        /// </para>
267        /// </remarks>
268        /// <typeparam name="T"></typeparam>
269        /// <param name="table">The table.</param>
270        /// <param name="objectSet">The object set.</param>
271        /// <param name="mode">The mode.</param>
272        public static void ReAttach<T>(this Table<T> table, IEnumerable<T> objectSet, RefreshMode mode) where T : class
273        {
274            ReAttachWithRefresh(table, objectSet, true, mode);
275        }
276
277
278        /// <summary>
279        /// Attaches a disconnected entity to a new <see>DataContext</see>.
280        /// </summary>
281        /// <remarks>
282        /// <para>
283        /// The default behavior, unless overridden, is to attach all members of the provided object graph to the <see>DataContext</see>, 
284        /// retaining current values.
285        /// </para>
286        /// <para>
287        /// The objective of this function is to simplify the use of LINQ to SQL in detached or semi-attached scenarios such as ASP.Net or other 
288        /// tiered architectures.
289        /// </para>
290        /// </remarks>
291        /// <typeparam name="T"></typeparam>
292        /// <param name="table">The table.</param>
293        /// <param name="objectSet">The object set.</param>
294        /// <param name="mode">The mode.</param>
295        /// <param name="entireGraph">if set to <c>true</c> [entire graph].</param>
296        public static void ReAttach<T>(this Table<T> table, IEnumerable<T> objectSet, bool entireGraph, RefreshMode mode) where T : class
297        {
298            ReAttachWithRefresh(table, objectSet, entireGraph, mode);
299        }
300
301
302        private static void ReAttachNoRefresh<T>(this Table<T> table, IEnumerable<T> objectSet, bool entireGraph) where T : class
303        {
304            if (objectSet == null)
305                return;
306           
307            if (objectSet.Count() == 0)
308                return;
309
310
311            List<object> attached = new List<object>();
312            foreach (T entity in objectSet)
313            {
314                if (entireGraph)
315                    GetAttachmentGraph(table, entity, attached, true);
316                else
317                {
318                    table.Attach(entity, true);
319                    attached.Add(entity);
320                }
321            }
322        }
323
324
325        public static void ReAttachWithRefresh<T>(this Table<T> table, IEnumerable<T> objectSet, bool entireGraph, RefreshMode mode) where T : class
326        {
327            if (objectSet == null)
328                return;
329
330
331            if (objectSet.Count() == 0)
332                return;
333
334
335            List<object> attached = new List<object>();
336            foreach (T entity in objectSet)
337            {
338                if (entireGraph)
339                    GetAttachmentGraph(table, entity, attached, false);
340                else
341                {
342                    table.Attach(entity);
343                    attached.Add(entity);
344                }
345            }
346            table.Context.Refresh(mode, attached);
347        }
348
349
350        #endregion Re-attach to Table<T>
351
352
353        #endregion Extension Methods
354
355
356        #region Utility Methods
357
358
359        private static void GetAttachmentGraph<T>(ITable table, T entity, List<object> attached, bool attachAsChanged) where T : class
360        {
361            // Loop over the collection of key associations on the table provided
362            foreach (MetaAssociation a in table.Context.Mapping.GetTable(entity.GetType()).RowType.Associations)
363            {
364                if (!a.IsForeignKey)
365                {
366                    // This is the "parent" class - get the child type and retrieve its value
367                    Type memberType = null;
368                    IEnumerable memberList = null;
369                    GetChildMemberData<T>(entity, a.ThisMember.Name, ref memberType, ref memberList);
370
371
372                    // If the member type was not returned, move on to the next association.
373                    if (memberType == null)
374                        continue;
375
376
377                    // Fetch the child's table from the DataContext and recurse to get all objects attached
378                    ITable childTable = table.Context.GetTable(memberType);
379                    if (childTable != null && memberList != null)
380                    {
381                        foreach (object child in memberList)
382                            GetAttachmentGraph(childTable, child, attached, attachAsChanged);
383                    }
384
385
386                }
387            }
388
389
390            // Attach the root entity after recursion
391            if (attachAsChanged)
392                table.Attach(entity, true);
393            else
394                table.Attach(entity);
395
396
397            // Add the attached entity to a collection for use during call to DataContext.Refresh
398            attached.Add(entity);
399        }
400
401
402        private static void GetChildMemberData<T>(T entity, string childMemberName, ref Type memberType, ref IEnumerable memberList) where T : class
403        {
404            // Validate inputs
405            if (entity == null || string.IsNullOrEmpty(childMemberName))
406                return;
407
408
409            // Fetch the child property information and determine whether it's generic; retrieve appropriate entity type.
410            PropertyInfo propInfo = GetPropertyInfoFromCache(entity.GetType(), childMemberName);
411
412
413            if (propInfo != null)
414            {
415                Type propertyType = propInfo.PropertyType;
416                if (propertyType.IsGenericType)
417                {
418                    Type[] args = propertyType.GetGenericArguments();
419                    if (args.Length == 1)
420                    {
421                        // Only enumerate single-dimensional generics (lists, collections etc.)
422                        memberType = args[0];
423                    }
424                }
425                else
426                {
427                    memberType = propInfo.PropertyType;
428                }
429
430
431                // Fetch the value of the property for enumeration
432                memberList = propInfo.GetValue(entity, null) as IEnumerable;
433            }
434        }
435
436
437        private static PropertyInfo GetPropertyInfoFromCache(Type entityType, string childMemberName)
438        {
439            BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
440            PropertyInfo result = null;
441
442
443            // Check to see if property info is in static cache.  If so, use it to avoid further reflection
444            if (_propertyCache.ContainsKey(entityType) && _propertyCache[entityType].ContainsKey(childMemberName))
445            {
446                // It's in the cache
447                result = _propertyCache[entityType][childMemberName];
448            }
449            else
450            {
451                // It's not in the cache; do reflection and add info to the cache
452                result = entityType.GetProperty(childMemberName, flags);
453                if (!_propertyCache.ContainsKey(entityType))
454                    _propertyCache.Add(entityType, new Dictionary<string, PropertyInfo>());
455                _propertyCache[entityType].Add(childMemberName, result);
456            }
457            return result;
458        }
459
460
461        #endregion Utility Methods
462    }
463}

Okay: "table.Context.Mapping.GetTable(entity.GetType()).RowType.Associations"?  Really?  Could that have been buried any deeper?  All I can say is I'm thankful for QuickWatch and .Net Reflector.  Otherwise I'd still be looking at this.

That's probably enough for now.  I'll probably blog more about the ultimate outcome of this after a decision is made at work, but for now, this kind of crap puts L2S closer to the "cute toy" category for me.

 

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Comments

Creative Commons License
This work is licensed under a Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License.