I've been playing around with the LINQ to SQL framework in .NET 3.5 for mapping classes to a SQL database. Exposing your data model through LINQ-able classes gives you the ability to write strongly typed queries in your code that are later translated to the necessary SQL commands. The framework materializes query results into objects for you, and provides methods for inserting, updating and deleting data. For a nice quick overview, I suggest checking out the LINQ to SQL - 5 Minute Overview.
One issue that I keep seeing with the DataContext class in this framework is the ability to use entities materialized in one context within another context.
Consider a simple application that supports logging a user in and having users post content.
//a static login method for the Account class public static Account Login(string email, string password) { using (CommunityDataContext dc = new CommunityDataContext()) { //lookup the account Account ExistingAccount = (from a in dc.Accounts where a.Email == email select a).FirstOrDefault(); if (ExistingAccount == null //username not found || ExistingAccount.Password != password) //password doesn't match return null; //update the last seen timestamp ExistingAccount.LastSeen = DateTime.UtcNow; dc.SubmitChanges(); return ExistingAccount; } } //some driver code Account User = Account.Login("someone@somewhere.com", "password"); if (User == null) throw new Exception("Login Failed."); using (CommunityDataContext dc = new CommunityDataContext()) { Post NewPost = new Post() { Account = User, Content = "Test Content" }; dc.Posts.InsertOnSubmit(NewPost); dc.SubmitChanges(); }
If we run this code, we'll find that an insert is attempted. What is being inserted might surprise you! Here is the log output from the DataContext:
DECLARE @output TABLE([AccountID] UniqueIdentifier) DECLARE @id UniqueIdentifier INSERT INTO [dbo].[Account]([Email], [Password], [LastSeen]) OUTPUT INSERTED.[AccountID] INTO @output VALUES (@p0, @p1, @p2) SELECT @id = AccountID FROM @output SELECT [t0].[AccountID], [t0].[Created] FROM [dbo].[Account] AS [t0] WHERE [t0].[AccountID] = (@id) -- @p0: Input VarChar (Size = 21; Prec = 0; Scale = 0) [someone@somewhere.com] -- @p1: Input VarChar (Size = 8; Prec = 0; Scale = 0) [password] -- @p2: Input SmallDateTime (Size = 0; Prec = 0; Scale = 0) [8/24/2008 8:09:35 PM] -- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.30428.1
You can see that we are trying to insert an Account instance, specifically the one I logged in as! Execution is haulted at this point within my code as an exception is raised (I have created a unique key constraint for account email addresses).
The issue is that the DataContext is treating the Account instance as a new account and attempting to insert it before inserting the new Post.
To prevent this behavior, inevitably the Attach method comes up. We might decide to try the following:
using (CommunityDataContext dc = new CommunityDataContext()) { dc.Accounts.Attach(User); Post NewPost = new Post()...
And now we reach the crux of our problem. The call to Attach raises a NotSupportException: An attempt has been made to Attach or Add an entity that is not new, perhaps having been loaded from another DataContext. This is not supported.
So there are many solutions that I've read about, just try searching Google. They range from:
- Passing your DataContext instance to each of your methods so that a single instance is used,
- Creating an ambient DataContext that is re-used automatically (avoids the extra argument required above),
- Defining a custom Detach method for each of your entity classes that erases any connection to the original DataContext instance,
- Defining a generic Detach method that uses reflection to produce a copy of your entity without any connection to the original DataContext,
- Defining a generic Detach method that uses serialization to produce a copy of your entity.
None of these really seem like adequate solutions to me. They all introduce overhead (reflection or serialization) and/or complexity (extra arguments or maintaining a custom methods). The ambient DataContext solution seemed intriguing at first, but also presents its own problems.
So after playing around with the framework some more, I've settled on my own solution: define a generic Detach method that copies the entity in the same way that entities are materialized. Namely, I'm going to use the MetaModel provided by the DataContext class. Here is Detach, provided by an ExtendedDataContext:
public class ExtendedDataContext : DataContext { /// <summary> /// Detaches an entity from its existing data context. /// </summary> /// <typeparam name="TEntity">The entity type.</typeparam> /// <param name="entity">The entity instance. /// <returns>A copy of the entity in a detached state.</returns> public TEntity Detach<TEntity>( TEntity entity) where TEntity : class, new() { if (entity == null) return null; //create a copy of the entity object Copy = new TEntity(); //enumerate the data member mappings for the entity type foreach (MetaDataMember member in Mapping.GetMetaType(typeof(TEntity)).DataMembers) { if (member.IsAssociation //skip associations and deferred members || member.IsDeferred) continue; //copy the member value member.StorageAccessor.SetBoxedValue( ref Copy, member.StorageAccessor.GetBoxedValue(entity)); } return (TEntity)Copy; } }
The algorithm is pretty concise: create a new instance of your entity and use its model metadata to copy data from the existing entity to the new instance. While copying, we skip associations and deferred members to avoid copying references to the original DataContext instance.
Please notice that the constructor for your entity will be called each time the Detach method is called. This means that the OnCreated extension method will also be called. This may or may not be a problem for you, depending on what code, if any, you define for the OnCreated extension. I provide another method to help transfer ownership of an entity from one DataContext to another, the Assume method:
/// <summary> /// Detaches an entity from its existing data context and re-attaches it to this data context. /// </summary> /// <typeparam name="TEntity">The entity type.</typeparam> /// <param name="entity">The entity instance, which will be replaced by the detached instance. /// <remarks>The entity instance is copied by this method.</remarks> public void Assume<TEntity>( ref TEntity entity) where TEntity : class, new() { if (entity == null) return; entity = Detach(entity); GetTable<TEntity>().Attach(entity); }
Going back to our original application, we can use the designer to change the base class of the generated CommunityDataContext to ExtendedDataContext, and change our driver code to the following:
Account User = Account.Login("someone@somewhere.com", "password"); if (User == null) throw new Exception("Login Failed."); using (CommunityDataContext dc = new CommunityDataContext()) { dc.Assume(ref User); Post NewPost = new Post() { Account = User, Content = "Test Content" }; dc.Posts.InsertOnSubmit(NewPost); dc.SubmitChanges(); }
The above will now work as expected! We can log in and get an Account instance that was created by one DataContext, and use that Account when creating a new Post in a later context.
I think that there are some real strengths to this approach.
- You don't need to maintain any additional code to support each of your entities,
- You can utilize the existing information provided by the MetaModel to avoid examining the entity type using reflection,
- This method works for entities mapped using either the AttributeMappingSource or the XmlMappingSource.
If you dig through the code used by the LINQ to SQL framework, you'll see the MetaModel and related classes are used everywhere. In particular, the framework uses the model to compile classes at runtime that extend System.Data.Linq.SqlClientImplementation.ObjectMaterializer<> to provide faster materialization of entities (without reflection). Perhaps a future extension of this method will do the same.
3 comments:
Great stuff - thanks! I modified the method to be an extension instead of messing around with the model. I posted that here.
Hi michel,
can u post the same code in VB.NET?
Thank's
Thank You and that i have a keen supply: How Much For House Renovation Uk small home renovations
Post a Comment