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
) 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.