5 // Copyright (c) 2007-2008 Jiri Moudry, Pascal Craponne
\r
7 // Permission is hereby granted, free of charge, to any person obtaining a copy
\r
8 // of this software and associated documentation files (the "Software"), to deal
\r
9 // in the Software without restriction, including without limitation the rights
\r
10 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
\r
11 // copies of the Software, and to permit persons to whom the Software is
\r
12 // furnished to do so, subject to the following conditions:
\r
14 // The above copyright notice and this permission notice shall be included in
\r
15 // all copies or substantial portions of the Software.
\r
17 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
\r
18 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
\r
19 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
\r
20 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
\r
21 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
\r
22 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
\r
28 using System.Collections;
\r
30 using System.Data.Common;
\r
31 using System.Data.Linq;
\r
32 using System.Data.Linq.Mapping;
\r
33 using System.Linq.Expressions;
\r
34 using System.Collections.Generic;
\r
37 using System.Reflection;
\r
38 using System.Reflection.Emit;
\r
41 using AttributeMappingSource = System.Data.Linq.Mapping.AttributeMappingSource;
\r
43 using AttributeMappingSource = DbLinq.Data.Linq.Mapping.AttributeMappingSource;
\r
47 using DbLinq.Data.Linq;
\r
48 using DbLinq.Data.Linq.Database;
\r
49 using DbLinq.Data.Linq.Database.Implementation;
\r
50 using DbLinq.Data.Linq.Identity;
\r
51 using DbLinq.Data.Linq.Implementation;
\r
52 using DbLinq.Data.Linq.Mapping;
\r
53 using DbLinq.Data.Linq.Sugar;
\r
54 using DbLinq.Factory;
\r
56 using DbLinq.Vendor;
\r
59 namespace System.Data.Linq
\r
61 namespace DbLinq.Data.Linq
\r
64 public partial class DataContext : IDisposable
\r
66 //private readonly Dictionary<string, ITable> _tableMap = new Dictionary<string, ITable>();
\r
67 private readonly Dictionary<Type, ITable> _tableMap = new Dictionary<Type, ITable>();
\r
69 public MetaModel Mapping { get; private set; }
\r
70 // PC question: at ctor, we get a IDbConnection and the Connection property exposes a DbConnection
\r
72 public DbConnection Connection { get { return DatabaseContext.Connection as DbConnection; } }
\r
74 // all properties below are set public to optionally be injected
\r
75 internal IVendor Vendor { get; set; }
\r
76 internal IQueryBuilder QueryBuilder { get; set; }
\r
77 internal IQueryRunner QueryRunner { get; set; }
\r
78 internal IMemberModificationHandler MemberModificationHandler { get; set; }
\r
79 internal IDatabaseContext DatabaseContext { get; private set; }
\r
80 // /all properties...
\r
82 private bool objectTrackingEnabled = true;
\r
83 private bool deferredLoadingEnabled = true;
\r
85 private bool queryCacheEnabled = false;
\r
88 /// Disable the QueryCache: this is surely good for rarely used Select, since preparing
\r
89 /// the SelectQuery to be cached could require more time than build the sql from scratch.
\r
92 public bool QueryCacheEnabled
\r
94 get { return queryCacheEnabled; }
\r
95 set { queryCacheEnabled = value; }
\r
98 private IEntityTracker currentTransactionEntities;
\r
99 private IEntityTracker CurrentTransactionEntities
\r
103 if (this.currentTransactionEntities == null)
\r
105 if (this.ObjectTrackingEnabled)
\r
106 this.currentTransactionEntities = new EntityTracker();
\r
108 this.currentTransactionEntities = new DisabledEntityTracker();
\r
110 return this.currentTransactionEntities;
\r
114 private IEntityTracker allTrackedEntities;
\r
115 private IEntityTracker AllTrackedEntities
\r
119 if (this.allTrackedEntities == null)
\r
121 allTrackedEntities = ObjectTrackingEnabled
\r
122 ? (IEntityTracker) new EntityTracker()
\r
123 : (IEntityTracker) new DisabledEntityTracker();
\r
125 return this.allTrackedEntities;
\r
129 private IIdentityReaderFactory identityReaderFactory;
\r
130 private readonly IDictionary<Type, IIdentityReader> identityReaders = new Dictionary<Type, IIdentityReader>();
\r
133 /// The default behavior creates one MappingContext.
\r
136 internal virtual MappingContext _MappingContext { get; set; }
\r
139 internal IVendorProvider _VendorProvider { get; set; }
\r
141 public DataContext(IDbConnection connection, MappingSource mapping)
\r
143 Profiler.At("START DataContext(IDbConnection, MappingSource)");
\r
144 Init(new DatabaseContext(connection), mapping, null);
\r
145 Profiler.At("END DataContext(IDbConnection, MappingSource)");
\r
148 public DataContext(IDbConnection connection)
\r
150 Profiler.At("START DataContext(IDbConnection)");
\r
151 if (connection == null)
\r
152 throw new ArgumentNullException("connection");
\r
154 Init(new DatabaseContext(connection), null, null);
\r
155 Profiler.At("END DataContext(IDbConnection)");
\r
159 public DataContext(string fileOrServerOrConnection, MappingSource mapping)
\r
161 Profiler.At("START DataContext(string, MappingSource)");
\r
162 if (fileOrServerOrConnection == null)
\r
163 throw new ArgumentNullException("fileOrServerOrConnection");
\r
164 if (mapping == null)
\r
165 throw new ArgumentNullException("mapping");
\r
167 if (File.Exists(fileOrServerOrConnection))
\r
168 throw new NotImplementedException("File names not supported.");
\r
170 // Is this a decent server name check?
\r
171 // It assumes that the connection string will have at least 2
\r
172 // parameters (separated by ';')
\r
173 if (!fileOrServerOrConnection.Contains(";"))
\r
174 throw new NotImplementedException("Server name not supported.");
\r
176 // Assume it's a connection string...
\r
177 IVendor ivendor = GetVendor(ref fileOrServerOrConnection);
\r
179 IDbConnection dbConnection = ivendor.CreateDbConnection(fileOrServerOrConnection);
\r
180 Init(new DatabaseContext(dbConnection), mapping, ivendor);
\r
181 Profiler.At("END DataContext(string, MappingSource)");
\r
185 /// Construct DataContext, given a connectionString.
\r
186 /// To determine which DB type to go against, we look for 'DbLinqProvider=xxx' substring.
\r
187 /// If not found, we assume that we are dealing with MS Sql Server.
\r
189 /// Valid values are names of provider DLLs (or any other DLL containing an IVendor implementation)
\r
190 /// DbLinqProvider=Mysql
\r
191 /// DbLinqProvider=Oracle etc.
\r
193 /// <param name="connectionString">specifies file or server connection</param>
\r
195 public DataContext(string connectionString)
\r
197 Profiler.At("START DataContext(string)");
\r
198 IVendor ivendor = GetVendor(ref connectionString);
\r
200 IDbConnection dbConnection = ivendor.CreateDbConnection(connectionString);
\r
201 Init(new DatabaseContext(dbConnection), null, ivendor);
\r
203 Profiler.At("END DataContext(string)");
\r
206 private IVendor GetVendor(ref string connectionString)
\r
208 if (connectionString == null)
\r
209 throw new ArgumentNullException("connectionString");
\r
212 string vendorClassToLoad;
\r
213 GetVendorInfo(ref connectionString, out assy, out vendorClassToLoad);
\r
216 from type in assy.GetTypes()
\r
217 where type.Name.ToLowerInvariant() == vendorClassToLoad.ToLowerInvariant() &&
\r
218 type.GetInterfaces().Contains(typeof(IVendor)) &&
\r
219 type.GetConstructor(Type.EmptyTypes) != null
\r
223 throw new ArgumentException(string.Format("Found no IVendor class in assembly `{0}' named `{1}' having a default constructor.",
\r
224 assy.GetName().Name, vendorClassToLoad));
\r
226 else if (types.Count() > 1)
\r
228 throw new ArgumentException(string.Format("Found too many IVendor classes in assembly `{0}' named `{1}' having a default constructor.",
\r
229 assy.GetName().Name, vendorClassToLoad));
\r
231 return (IVendor) Activator.CreateInstance(types.First());
\r
234 private void GetVendorInfo(ref string connectionString, out Assembly assembly, out string typeName)
\r
236 System.Text.RegularExpressions.Regex reProvider
\r
237 = new System.Text.RegularExpressions.Regex(@"DbLinqProvider=([\w\.]+);?");
\r
239 string assemblyFile = null;
\r
241 if (!reProvider.IsMatch(connectionString))
\r
243 vendor = "SqlServer";
\r
244 assemblyFile = "DbLinq.SqlServer.dll";
\r
248 var match = reProvider.Match(connectionString);
\r
249 vendor = match.Groups[1].Value;
\r
250 assemblyFile = "DbLinq." + vendor + ".dll";
\r
252 //plain DbLinq - non MONO:
\r
253 //IVendor classes are in DLLs such as "DbLinq.MySql.dll"
\r
254 if (vendor.Contains("."))
\r
256 //already fully qualified DLL name?
\r
257 throw new ArgumentException("Please provide a short name, such as 'MySql', not '" + vendor + "'");
\r
260 //shorten: "DbLinqProvider=X;Server=Y" -> ";Server=Y"
\r
261 connectionString = reProvider.Replace(connectionString, "");
\r
264 typeName = vendor + "Vendor";
\r
269 assembly = typeof (DataContext).Assembly; // System.Data.Linq.dll
\r
271 //TODO: check if DLL is already loaded?
\r
272 assembly = Assembly.LoadFrom(assemblyFile);
\r
275 catch (Exception e)
\r
277 throw new ArgumentException(
\r
279 "Unable to load the `{0}' DbLinq vendor within assembly `{1}'.",
\r
280 assemblyFile, vendor),
\r
281 "connectionString", e);
\r
285 private void Init(IDatabaseContext databaseContext, MappingSource mappingSource, IVendor vendor)
\r
287 if (databaseContext == null)
\r
288 throw new ArgumentNullException("databaseContext");
\r
290 // Yes, .NET throws an NRE for this. Why it's not ArgumentNullException, I couldn't tell you.
\r
291 if (databaseContext.Connection.ConnectionString == null)
\r
292 throw new NullReferenceException();
\r
294 string connectionString = databaseContext.Connection.ConnectionString;
\r
295 _VendorProvider = ObjectFactory.Get<IVendorProvider>();
\r
296 Vendor = vendor ??
\r
297 (connectionString != null ? GetVendor(ref connectionString) : null) ??
\r
298 _VendorProvider.FindVendorByProviderType(typeof(SqlClient.Sql2005Provider));
\r
300 DatabaseContext = databaseContext;
\r
302 MemberModificationHandler = ObjectFactory.Create<IMemberModificationHandler>(); // not a singleton: object is stateful
\r
303 QueryBuilder = ObjectFactory.Get<IQueryBuilder>();
\r
304 QueryRunner = ObjectFactory.Get<IQueryRunner>();
\r
306 //EntityMap = ObjectFactory.Create<IEntityMap>();
\r
307 identityReaderFactory = ObjectFactory.Get<IIdentityReaderFactory>();
\r
309 _MappingContext = new MappingContext();
\r
311 // initialize the mapping information
\r
312 if (mappingSource == null)
\r
313 mappingSource = new AttributeMappingSource();
\r
314 Mapping = mappingSource.GetModel(GetType());
\r
318 /// Checks if the table is allready mapped or maps it if not.
\r
320 /// <param name="tableType">Type of the table.</param>
\r
321 /// <exception cref="InvalidOperationException">Thrown if the table is not mappable.</exception>
\r
322 private void CheckTableMapping(Type tableType)
\r
324 //This will throw an exception if the table is not found
\r
325 if(Mapping.GetTable(tableType) == null)
\r
327 throw new InvalidOperationException("The type '" + tableType.Name + "' is not mapped as a Table.");
\r
332 /// Returns a Table for the type TEntity.
\r
334 /// <exception cref="InvalidOperationException">If the type TEntity is not mappable as a Table.</exception>
\r
335 /// <typeparam name="TEntity">The table type.</typeparam>
\r
336 public Table<TEntity> GetTable<TEntity>() where TEntity : class
\r
338 return (Table<TEntity>)GetTable(typeof(TEntity));
\r
342 /// Returns a Table for the given type.
\r
344 /// <param name="type">The table type.</param>
\r
345 /// <exception cref="InvalidOperationException">If the type is not mappable as a Table.</exception>
\r
346 public ITable GetTable(Type type)
\r
348 Profiler.At("DataContext.GetTable(typeof({0}))", type != null ? type.Name : null);
\r
349 ITable tableExisting;
\r
350 if (_tableMap.TryGetValue(type, out tableExisting))
\r
351 return tableExisting;
\r
353 //Check for table mapping
\r
354 CheckTableMapping(type);
\r
356 var tableNew = Activator.CreateInstance(
\r
357 typeof(Table<>).MakeGenericType(type)
\r
358 , BindingFlags.NonPublic | BindingFlags.Instance
\r
360 , new object[] { this }
\r
361 , System.Globalization.CultureInfo.CurrentCulture) as ITable;
\r
363 _tableMap[type] = tableNew;
\r
367 public void SubmitChanges()
\r
369 SubmitChanges(ConflictMode.FailOnFirstConflict);
\r
375 /// <returns></returns>
\r
376 public bool DatabaseExists()
\r
380 return Vendor.Ping(this);
\r
389 /// Commits all pending changes to database
\r
391 /// <param name="failureMode"></param>
\r
392 public virtual void SubmitChanges(ConflictMode failureMode)
\r
394 if (this.objectTrackingEnabled == false)
\r
395 throw new InvalidOperationException("Object tracking is not enabled for the current data context instance.");
\r
396 using (DatabaseContext.OpenConnection()) //ConnMgr will close connection for us
\r
398 if (Transaction != null)
\r
399 SubmitChangesImpl(failureMode);
\r
402 using (IDatabaseTransaction transaction = DatabaseContext.Transaction())
\r
406 Transaction = (DbTransaction) transaction.Transaction;
\r
407 SubmitChangesImpl(failureMode);
\r
408 // TODO: handle conflicts (which can only occur when concurrency mode is implemented)
\r
409 transaction.Commit();
\r
413 Transaction = null;
\r
420 void SubmitChangesImpl(ConflictMode failureMode)
\r
422 var queryContext = new QueryContext(this);
\r
424 // There's no sense in updating an entity when it's going to
\r
425 // be deleted in the current transaction, so do deletes first.
\r
426 foreach (var entityTrack in CurrentTransactionEntities.EnumerateAll().ToList())
\r
428 switch (entityTrack.EntityState)
\r
430 case EntityState.ToDelete:
\r
431 var deleteQuery = QueryBuilder.GetDeleteQuery(entityTrack.Entity, queryContext);
\r
432 QueryRunner.Delete(entityTrack.Entity, deleteQuery);
\r
434 UnregisterDelete(entityTrack.Entity);
\r
435 AllTrackedEntities.RegisterToDelete(entityTrack.Entity);
\r
436 AllTrackedEntities.RegisterDeleted(entityTrack.Entity);
\r
443 foreach (var entityTrack in CurrentTransactionEntities.EnumerateAll()
\r
444 .Concat(AllTrackedEntities.EnumerateAll())
\r
447 switch (entityTrack.EntityState)
\r
449 case EntityState.ToInsert:
\r
450 foreach (var toInsert in GetReferencedObjects(entityTrack.Entity))
\r
452 InsertEntity(toInsert, queryContext);
\r
455 case EntityState.ToWatch:
\r
456 foreach (var toUpdate in GetReferencedObjects(entityTrack.Entity))
\r
458 UpdateEntity(toUpdate, queryContext);
\r
462 throw new ArgumentOutOfRangeException();
\r
467 private IEnumerable<object> GetReferencedObjects(object value)
\r
469 var values = new EntitySet<object>();
\r
470 FillReferencedObjects(value, values);
\r
474 // Breadth-first traversal of an object graph
\r
475 private void FillReferencedObjects(object parent, EntitySet<object> values)
\r
477 if (parent == null)
\r
479 var children = new Queue<object>();
\r
480 children.Enqueue(parent);
\r
481 while (children.Count > 0)
\r
483 object value = children.Dequeue();
\r
485 IEnumerable<MetaAssociation> associationList = Mapping.GetMetaType(value.GetType()).Associations.Where(a => !a.IsForeignKey);
\r
486 if (associationList.Any())
\r
488 foreach (MetaAssociation association in associationList)
\r
490 var memberData = association.ThisMember;
\r
491 var entitySetValue = memberData.Member.GetMemberValue(value);
\r
493 if (entitySetValue != null)
\r
495 var hasLoadedOrAssignedValues = entitySetValue.GetType().GetProperty("HasLoadedOrAssignedValues");
\r
496 if (!((bool)hasLoadedOrAssignedValues.GetValue(entitySetValue, null)))
\r
497 continue; // execution deferred; ignore.
\r
498 foreach (var o in ((IEnumerable)entitySetValue))
\r
499 children.Enqueue(o);
\r
506 private void InsertEntity(object entity, QueryContext queryContext)
\r
508 var insertQuery = QueryBuilder.GetInsertQuery(entity, queryContext);
\r
509 QueryRunner.Insert(entity, insertQuery);
\r
511 UpdateReferencedObjects(entity);
\r
512 MoveToAllTrackedEntities(entity, true);
\r
515 private void UpdateEntity(object entity, QueryContext queryContext)
\r
517 if (!AllTrackedEntities.ContainsReference(entity))
\r
518 InsertEntity(entity, queryContext);
\r
519 else if (MemberModificationHandler.IsModified(entity, Mapping))
\r
521 var modifiedMembers = MemberModificationHandler.GetModifiedProperties(entity, Mapping);
\r
522 var updateQuery = QueryBuilder.GetUpdateQuery(entity, modifiedMembers, queryContext);
\r
523 QueryRunner.Update(entity, updateQuery, modifiedMembers);
\r
525 RegisterUpdateAgain(entity);
\r
526 UpdateReferencedObjects(entity);
\r
527 MoveToAllTrackedEntities(entity, false);
\r
531 private void UpdateReferencedObjects(object root)
\r
533 var metaType = Mapping.GetMetaType(root.GetType());
\r
534 foreach (var assoc in metaType.Associations)
\r
536 var memberData = assoc.ThisMember;
\r
537 //This is not correct - AutoSyncing applies to auto-updating columns, such as a TimeStamp, not to foreign key associations, which is always automatically synched
\r
538 //Confirmed against default .NET l2sql - association columns are always set, even if AutoSync==AutoSync.Never
\r
539 //if (memberData.Association.ThisKey.Any(m => (m.AutoSync != AutoSync.Always) && (m.AutoSync != sync)))
\r
541 var oks = memberData.Association.OtherKey.Select(m => m.StorageMember).ToList();
\r
542 if (oks.Count == 0)
\r
544 var pks = memberData.Association.ThisKey
\r
545 .Select(m => m.StorageMember.GetMemberValue(root))
\r
547 if (pks.Count != oks.Count)
\r
548 throw new InvalidOperationException(
\r
549 string.Format("Count of primary keys ({0}) doesn't match count of other keys ({1}).",
\r
550 pks.Count, oks.Count));
\r
551 var members = memberData.Member.GetMemberValue(root) as IEnumerable;
\r
552 if (members == null)
\r
554 foreach (var member in members)
\r
556 for (int i = 0; i < pks.Count; ++i)
\r
558 oks[i].SetMemberValue(member, pks[i]);
\r
564 private void MoveToAllTrackedEntities(object entity, bool insert)
\r
566 if (!ObjectTrackingEnabled)
\r
568 if (CurrentTransactionEntities.ContainsReference(entity))
\r
570 CurrentTransactionEntities.RegisterToDelete(entity);
\r
572 CurrentTransactionEntities.RegisterDeleted(entity);
\r
574 if (!AllTrackedEntities.ContainsReference(entity))
\r
576 var identityReader = _GetIdentityReader(entity.GetType());
\r
577 AllTrackedEntities.RegisterToWatch(entity, identityReader.GetIdentityKey(entity));
\r
582 /// TODO - allow generated methods to call into stored procedures
\r
585 internal IExecuteResult _ExecuteMethodCall(DataContext context, System.Reflection.MethodInfo method, params object[] sqlParams)
\r
587 using (DatabaseContext.OpenConnection())
\r
589 System.Data.Linq.IExecuteResult result = Vendor.ExecuteMethodCall(context, method, sqlParams);
\r
595 protected IExecuteResult ExecuteMethodCall(object instance, System.Reflection.MethodInfo methodInfo, params object[] parameters)
\r
597 throw new NotImplementedException();
\r
600 #region Identity management
\r
603 internal IIdentityReader _GetIdentityReader(Type t)
\r
605 IIdentityReader identityReader;
\r
606 if (!identityReaders.TryGetValue(t, out identityReader))
\r
608 identityReader = identityReaderFactory.GetReader(t, this);
\r
609 identityReaders[t] = identityReader;
\r
611 return identityReader;
\r
615 internal object _GetRegisteredEntity(object entity)
\r
617 // TODO: check what is faster: by identity or by ref
\r
618 var identityReader = _GetIdentityReader(entity.GetType());
\r
619 var identityKey = identityReader.GetIdentityKey(entity);
\r
620 if (identityKey == null) // if we don't have an entitykey here, it means that the entity has no PK
\r
623 var registeredEntityTrack =
\r
624 CurrentTransactionEntities.FindByIdentity(identityKey) ??
\r
625 AllTrackedEntities.FindByIdentity(identityKey);
\r
626 if (registeredEntityTrack != null)
\r
627 return registeredEntityTrack.Entity;
\r
631 //internal object GetRegisteredEntityByKey(IdentityKey identityKey)
\r
633 // return EntityMap[identityKey];
\r
637 /// Registers an entity in a watch state
\r
639 /// <param name="entity"></param>
\r
640 /// <returns></returns>
\r
642 internal object _GetOrRegisterEntity(object entity)
\r
644 var identityReader = _GetIdentityReader(entity.GetType());
\r
645 var identityKey = identityReader.GetIdentityKey(entity);
\r
646 SetEntitySetsQueries(entity);
\r
647 SetEntityRefQueries(entity);
\r
649 // if we have no identity, we can't track it
\r
650 if (identityKey == null)
\r
653 // try to find an already registered entity and return it
\r
654 var registeredEntityTrack =
\r
655 CurrentTransactionEntities.FindByIdentity(identityKey) ??
\r
656 AllTrackedEntities.FindByIdentity(identityKey);
\r
657 if (registeredEntityTrack != null)
\r
658 return registeredEntityTrack.Entity;
\r
660 // otherwise, register and return
\r
661 AllTrackedEntities.RegisterToWatch(entity, identityKey);
\r
665 readonly IDataMapper DataMapper = ObjectFactory.Get<IDataMapper>();
\r
666 private void SetEntityRefQueries(object entity)
\r
668 if (!this.deferredLoadingEnabled)
\r
671 // BUG: This is ignoring External Mappings from XmlMappingSource.
\r
673 Type thisType = entity.GetType();
\r
674 IEnumerable<MetaAssociation> associationList = Mapping.GetMetaType(entity.GetType()).Associations.Where(a => a.IsForeignKey);
\r
675 foreach (MetaAssociation association in associationList)
\r
677 //example of entityRef:Order.Employee
\r
678 var memberData = association.ThisMember;
\r
679 Type otherTableType = association.OtherType.Type;
\r
680 ParameterExpression p = Expression.Parameter(otherTableType, "other");
\r
682 var otherTable = GetTable(otherTableType);
\r
684 //ie:EmployeeTerritories.EmployeeID
\r
685 var foreignKeys = memberData.Association.ThisKey;
\r
686 BinaryExpression predicate = null;
\r
687 var otherPKs = memberData.Association.OtherKey;
\r
688 IEnumerator<MetaDataMember> otherPKEnumerator = otherPKs.GetEnumerator();
\r
690 if (otherPKs.Count != foreignKeys.Count)
\r
691 throw new InvalidOperationException("Foreign keys don't match ThisKey");
\r
692 foreach (MetaDataMember key in foreignKeys)
\r
694 otherPKEnumerator.MoveNext();
\r
696 var thisForeignKeyProperty = (PropertyInfo)key.Member;
\r
697 object thisForeignKeyValue = thisForeignKeyProperty.GetValue(entity, null);
\r
699 if (thisForeignKeyValue != null)
\r
701 BinaryExpression keyPredicate;
\r
702 if (!(thisForeignKeyProperty.PropertyType.IsNullable()))
\r
704 keyPredicate = Expression.Equal(Expression.MakeMemberAccess(p, otherPKEnumerator.Current.Member),
\r
705 Expression.Constant(thisForeignKeyValue));
\r
709 var ValueProperty = thisForeignKeyProperty.PropertyType.GetProperty("Value");
\r
710 keyPredicate = Expression.Equal(Expression.MakeMemberAccess(p, otherPKEnumerator.Current.Member),
\r
711 Expression.Constant(ValueProperty.GetValue(thisForeignKeyValue, null)));
\r
714 if (predicate == null)
\r
715 predicate = keyPredicate;
\r
717 predicate = Expression.And(predicate, keyPredicate);
\r
720 IEnumerable query = null;
\r
721 if (predicate != null)
\r
723 query = GetOtherTableQuery(predicate, p, otherTableType, otherTable) as IEnumerable;
\r
724 //it would be interesting surround the above query with a .Take(1) expression for performance.
\r
727 // If no separate Storage is specified, use the member directly
\r
728 MemberInfo storage = memberData.StorageMember;
\r
729 if (storage == null)
\r
730 storage = memberData.Member;
\r
732 // Check that the storage is a field or a writable property
\r
733 if (!(storage is FieldInfo) && !(storage is PropertyInfo && ((PropertyInfo)storage).CanWrite)) {
\r
734 throw new InvalidOperationException(String.Format(
\r
735 "Member {0}.{1} is not a field nor a writable property",
\r
736 storage.DeclaringType, storage.Name));
\r
739 Type storageType = storage.GetMemberType();
\r
741 object entityRefValue = null;
\r
743 entityRefValue = Activator.CreateInstance(storageType, query);
\r
745 entityRefValue = Activator.CreateInstance(storageType);
\r
747 storage.SetMemberValue(entity, entityRefValue);
\r
752 /// This method is executed when the entity is being registered. Each EntitySet property has a internal query that can be set using the EntitySet.SetSource method.
\r
753 /// Here we set the query source of each EntitySetProperty
\r
755 /// <param name="entity"></param>
\r
756 private void SetEntitySetsQueries(object entity)
\r
758 if (!this.deferredLoadingEnabled)
\r
761 // BUG: This is ignoring External Mappings from XmlMappingSource.
\r
763 IEnumerable<MetaAssociation> associationList = Mapping.GetMetaType(entity.GetType()).Associations.Where(a => !a.IsForeignKey);
\r
765 if (associationList.Any())
\r
767 foreach (MetaAssociation association in associationList)
\r
769 //example of entitySet: Employee.EmployeeTerritories
\r
770 var memberData = association.ThisMember;
\r
771 Type otherTableType = association.OtherType.Type;
\r
772 ParameterExpression p = Expression.Parameter(otherTableType, "other");
\r
774 //other table:EmployeeTerritories
\r
775 var otherTable = GetTable(otherTableType);
\r
777 var otherKeys = memberData.Association.OtherKey;
\r
778 var thisKeys = memberData.Association.ThisKey;
\r
779 if (otherKeys.Count != thisKeys.Count)
\r
780 throw new InvalidOperationException("This keys don't match OtherKey");
\r
781 BinaryExpression predicate = null;
\r
782 IEnumerator<MetaDataMember> thisKeyEnumerator = thisKeys.GetEnumerator();
\r
783 foreach (MetaDataMember otherKey in otherKeys)
\r
785 thisKeyEnumerator.MoveNext();
\r
786 //other table member:EmployeeTerritories.EmployeeID
\r
787 var otherTableMember = (PropertyInfo)otherKey.Member;
\r
789 BinaryExpression keyPredicate;
\r
790 if (!(otherTableMember.PropertyType.IsNullable()))
\r
792 keyPredicate = Expression.Equal(Expression.MakeMemberAccess(p, otherTableMember),
\r
793 Expression.Constant(thisKeyEnumerator.Current.Member.GetMemberValue(entity)));
\r
797 var ValueProperty = otherTableMember.PropertyType.GetProperty("Value");
\r
798 keyPredicate = Expression.Equal(Expression.MakeMemberAccess(
\r
799 Expression.MakeMemberAccess(p, otherTableMember),
\r
801 Expression.Constant(thisKeyEnumerator.Current.Member.GetMemberValue(entity)));
\r
803 if (predicate == null)
\r
804 predicate = keyPredicate;
\r
806 predicate = Expression.And(predicate, keyPredicate);
\r
809 var query = GetOtherTableQuery(predicate, p, otherTableType, otherTable);
\r
811 var entitySetValue = memberData.Member.GetMemberValue(entity);
\r
813 if (entitySetValue == null)
\r
815 entitySetValue = Activator.CreateInstance(memberData.Member.GetMemberType());
\r
816 memberData.Member.SetMemberValue(entity, entitySetValue);
\r
819 var hasLoadedOrAssignedValues = entitySetValue.GetType().GetProperty("HasLoadedOrAssignedValues");
\r
820 if ((bool)hasLoadedOrAssignedValues.GetValue(entitySetValue, null))
\r
823 var setSourceMethod = entitySetValue.GetType().GetMethod("SetSource");
\r
824 setSourceMethod.Invoke(entitySetValue, new[] { query });
\r
825 //employee.EmployeeTerritories.SetSource(Table[EmployeesTerritories].Where(other=>other.employeeID="WARTH"))
\r
830 private static MethodInfo _WhereMethod = typeof(Queryable).GetMethods().First(m => m.Name == "Where");
\r
831 internal object GetOtherTableQuery(Expression predicate, ParameterExpression parameter, Type otherTableType, IQueryable otherTable)
\r
833 //predicate: other.EmployeeID== "WARTH"
\r
834 Expression lambdaPredicate = Expression.Lambda(predicate, parameter);
\r
835 //lambdaPredicate: other=>other.EmployeeID== "WARTH"
\r
837 Expression call = Expression.Call(_WhereMethod.MakeGenericMethod(otherTableType), otherTable.Expression, lambdaPredicate);
\r
838 //Table[EmployeesTerritories].Where(other=>other.employeeID="WARTH")
\r
840 return otherTable.Provider.CreateQuery(call);
\r
845 #region Insert/Update/Delete management
\r
848 /// Registers an entity for insert
\r
850 /// <param name="entity"></param>
\r
851 internal void RegisterInsert(object entity)
\r
853 CurrentTransactionEntities.RegisterToInsert(entity);
\r
856 private void DoRegisterUpdate(object entity)
\r
858 if (entity == null)
\r
859 throw new ArgumentNullException("entity");
\r
861 if (!this.objectTrackingEnabled)
\r
864 var identityReader = _GetIdentityReader(entity.GetType());
\r
865 var identityKey = identityReader.GetIdentityKey(entity);
\r
866 // if we have no key, we can not watch
\r
867 if (identityKey == null || identityKey.Keys.Count == 0)
\r
870 AllTrackedEntities.RegisterToWatch(entity, identityKey);
\r
874 /// Registers an entity for update
\r
875 /// The entity will be updated only if some of its members have changed after the registration
\r
877 /// <param name="entity"></param>
\r
878 internal void RegisterUpdate(object entity)
\r
880 DoRegisterUpdate(entity);
\r
881 MemberModificationHandler.Register(entity, Mapping);
\r
885 /// Registers or re-registers an entity and clears its state
\r
887 /// <param name="entity"></param>
\r
888 /// <returns></returns>
\r
889 internal object Register(object entity)
\r
891 if (! this.objectTrackingEnabled)
\r
893 var registeredEntity = _GetOrRegisterEntity(entity);
\r
894 // the fact of registering again clears the modified state, so we're... clear with that
\r
895 MemberModificationHandler.Register(registeredEntity, Mapping);
\r
896 return registeredEntity;
\r
900 /// Registers an entity for update
\r
901 /// The entity will be updated only if some of its members have changed after the registration
\r
903 /// <param name="entity"></param>
\r
904 /// <param name="entityOriginalState"></param>
\r
905 internal void RegisterUpdate(object entity, object entityOriginalState)
\r
907 if (!this.objectTrackingEnabled)
\r
909 DoRegisterUpdate(entity);
\r
910 MemberModificationHandler.Register(entity, entityOriginalState, Mapping);
\r
914 /// Clears the current state, and marks the object as clean
\r
916 /// <param name="entity"></param>
\r
917 internal void RegisterUpdateAgain(object entity)
\r
919 if (!this.objectTrackingEnabled)
\r
921 MemberModificationHandler.ClearModified(entity, Mapping);
\r
925 /// Registers an entity for delete
\r
927 /// <param name="entity"></param>
\r
928 internal void RegisterDelete(object entity)
\r
930 if (!this.objectTrackingEnabled)
\r
932 CurrentTransactionEntities.RegisterToDelete(entity);
\r
936 /// Unregisters entity after deletion
\r
938 /// <param name="entity"></param>
\r
939 internal void UnregisterDelete(object entity)
\r
941 if (!this.objectTrackingEnabled)
\r
943 CurrentTransactionEntities.RegisterDeleted(entity);
\r
949 /// Changed object determine
\r
951 /// <returns>Lists of inserted, updated, deleted objects</returns>
\r
952 public ChangeSet GetChangeSet()
\r
954 var inserts = new List<object>();
\r
955 var updates = new List<object>();
\r
956 var deletes = new List<object>();
\r
957 foreach (var entityTrack in CurrentTransactionEntities.EnumerateAll()
\r
958 .Concat(AllTrackedEntities.EnumerateAll()))
\r
960 switch (entityTrack.EntityState)
\r
962 case EntityState.ToInsert:
\r
963 inserts.Add(entityTrack.Entity);
\r
965 case EntityState.ToWatch:
\r
966 if (MemberModificationHandler.IsModified(entityTrack.Entity, Mapping))
\r
967 updates.Add(entityTrack.Entity);
\r
969 case EntityState.ToDelete:
\r
970 deletes.Add(entityTrack.Entity);
\r
973 throw new ArgumentOutOfRangeException();
\r
976 return new ChangeSet(inserts, updates, deletes);
\r
980 /// use ExecuteCommand to call raw SQL
\r
982 public int ExecuteCommand(string command, params object[] parameters)
\r
984 var directQuery = QueryBuilder.GetDirectQuery(command, new QueryContext(this));
\r
985 return QueryRunner.Execute(directQuery, parameters);
\r
989 /// Execute raw SQL query and return object
\r
991 public IEnumerable<TResult> ExecuteQuery<TResult>(string query, params object[] parameters) where TResult : class, new()
\r
994 throw new ArgumentNullException("query");
\r
996 return CreateExecuteQueryEnumerable<TResult>(query, parameters);
\r
999 private IEnumerable<TResult> CreateExecuteQueryEnumerable<TResult>(string query, object[] parameters)
\r
1000 where TResult : class, new()
\r
1002 foreach (TResult result in ExecuteQuery(typeof(TResult), query, parameters))
\r
1003 yield return result;
\r
1006 public IEnumerable ExecuteQuery(Type elementType, string query, params object[] parameters)
\r
1008 if (elementType == null)
\r
1009 throw new ArgumentNullException("elementType");
\r
1010 if (query == null)
\r
1011 throw new ArgumentNullException("query");
\r
1013 var queryContext = new QueryContext(this);
\r
1014 var directQuery = QueryBuilder.GetDirectQuery(query, queryContext);
\r
1015 return QueryRunner.ExecuteSelect(elementType, directQuery, parameters);
\r
1019 /// Gets or sets the load options
\r
1022 public DataLoadOptions LoadOptions
\r
1024 get { throw new NotImplementedException(); }
\r
1025 set { throw new NotImplementedException(); }
\r
1028 public DbTransaction Transaction { get; set; }
\r
1031 /// Runs the given reader and returns columns.
\r
1033 /// <typeparam name="TResult">The type of the result.</typeparam>
\r
1034 /// <param name="reader">The reader.</param>
\r
1035 /// <returns></returns>
\r
1036 public IEnumerable<TResult> Translate<TResult>(DbDataReader reader)
\r
1038 if (reader == null)
\r
1039 throw new ArgumentNullException("reader");
\r
1040 return CreateTranslateIterator<TResult>(reader);
\r
1043 IEnumerable<TResult> CreateTranslateIterator<TResult>(DbDataReader reader)
\r
1045 foreach (TResult result in Translate(typeof(TResult), reader))
\r
1046 yield return result;
\r
1049 public IMultipleResults Translate(DbDataReader reader)
\r
1051 throw new NotImplementedException();
\r
1054 public IEnumerable Translate(Type elementType, DbDataReader reader)
\r
1056 if (elementType == null)
\r
1057 throw new ArgumentNullException("elementType");
\r
1058 if (reader == null)
\r
1059 throw new ArgumentNullException("reader");
\r
1061 return QueryRunner.EnumerateResult(elementType, reader, this);
\r
1064 public void Dispose()
\r
1066 //connection closing should not be done here.
\r
1067 //read: http://msdn2.microsoft.com/en-us/library/bb292288.aspx
\r
1069 //We own the instance of MemberModificationHandler - we must unregister listeners of entities we attached to
\r
1070 MemberModificationHandler.UnregisterAll();
\r
1074 protected virtual void Dispose(bool disposing)
\r
1076 throw new NotImplementedException();
\r
1080 /// Creates a IDbDataAdapter. Used internally by Vendors
\r
1082 /// <returns></returns>
\r
1083 internal IDbDataAdapter CreateDataAdapter()
\r
1085 return DatabaseContext.CreateDataAdapter();
\r
1089 /// Sets a TextWriter where generated SQL commands are written
\r
1091 public TextWriter Log { get; set; }
\r
1094 /// Writes text on Log (if not null)
\r
1095 /// Internal helper
\r
1097 /// <param name="text"></param>
\r
1098 internal void WriteLog(string text)
\r
1101 Log.WriteLine(text);
\r
1105 /// Write an IDbCommand to Log (if non null)
\r
1107 /// <param name="command"></param>
\r
1108 internal void WriteLog(IDbCommand command)
\r
1112 Log.WriteLine(command.CommandText);
\r
1113 foreach (IDbDataParameter parameter in command.Parameters)
\r
1114 WriteLog(parameter);
\r
1116 Log.Write(" Context: {0}", Vendor.VendorName);
\r
1117 Log.Write(" Model: {0}", Mapping.GetType().Name);
\r
1118 Log.Write(" Build: {0}", Assembly.GetExecutingAssembly().GetName().Version);
\r
1124 /// Writes and IDbDataParameter to Log (if non null)
\r
1126 /// <param name="parameter"></param>
\r
1127 internal void WriteLog(IDbDataParameter parameter)
\r
1131 // -- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [2]
\r
1132 // -- <name>: <direction> <type> (...) [<value>]
\r
1133 Log.WriteLine("-- {0}: {1} {2} (Size = {3}; Prec = {4}; Scale = {5}) [{6}]",
\r
1134 parameter.ParameterName, parameter.Direction, parameter.DbType,
\r
1135 parameter.Size, parameter.Precision, parameter.Scale, parameter.Value);
\r
1139 public bool ObjectTrackingEnabled
\r
1141 get { return this.objectTrackingEnabled; }
\r
1144 if (this.currentTransactionEntities != null && value != this.objectTrackingEnabled)
\r
1145 throw new InvalidOperationException("Data context options cannot be modified after results have been returned from a query.");
\r
1146 this.objectTrackingEnabled = value;
\r
1151 public int CommandTimeout
\r
1153 get { throw new NotImplementedException(); }
\r
1154 set { throw new NotImplementedException(); }
\r
1157 public bool DeferredLoadingEnabled
\r
1159 get { return this.deferredLoadingEnabled; }
\r
1162 if (this.currentTransactionEntities != null && value != this.deferredLoadingEnabled)
\r
1163 throw new InvalidOperationException("Data context options cannot be modified after results have been returned from a query.");
\r
1164 this.deferredLoadingEnabled = value;
\r
1169 public ChangeConflictCollection ChangeConflicts
\r
1171 get { throw new NotImplementedException(); }
\r
1175 public DbCommand GetCommand(IQueryable query)
\r
1177 DbCommand dbCommand = GetIDbCommand(query) as DbCommand;
\r
1178 if (dbCommand == null)
\r
1179 throw new InvalidOperationException();
\r
1185 public IDbCommand GetIDbCommand(IQueryable query)
\r
1187 if (query == null)
\r
1188 throw new ArgumentNullException("query");
\r
1190 var qp = query.Provider as QueryProvider;
\r
1192 throw new InvalidOperationException();
\r
1194 if (qp.ExpressionChain.Expressions.Count == 0)
\r
1195 qp.ExpressionChain.Expressions.Add(CreateDefaultQuery(query));
\r
1197 return qp.GetQuery(null).GetCommand().Command;
\r
1200 private Expression CreateDefaultQuery(IQueryable query)
\r
1202 // Manually create the expression tree for: IQueryable<TableType>.Select(e => e)
\r
1203 var identityParameter = Expression.Parameter(query.ElementType, "e");
\r
1204 var identityBody = Expression.Lambda(
\r
1205 typeof(Func<,>).MakeGenericType(query.ElementType, query.ElementType),
\r
1206 identityParameter,
\r
1207 new[] { identityParameter }
\r
1210 return Expression.Call(
\r
1211 typeof(Queryable),
\r
1213 new[] { query.ElementType, query.ElementType },
\r
1215 Expression.Quote(identityBody)
\r
1220 public void Refresh(RefreshMode mode, IEnumerable entities)
\r
1222 throw new NotImplementedException();
\r
1226 public void Refresh(RefreshMode mode, params object[] entities)
\r
1228 throw new NotImplementedException();
\r
1232 public void Refresh(RefreshMode mode, object entity)
\r
1234 throw new NotImplementedException();
\r
1238 public void DeleteDatabase()
\r
1240 throw new NotImplementedException();
\r
1244 public void CreateDatabase()
\r
1246 throw new NotImplementedException();
\r
1250 protected internal IQueryable<TResult> CreateMethodCallQuery<TResult>(object instance, MethodInfo methodInfo, params object[] parameters)
\r
1252 throw new NotImplementedException();
\r
1256 protected internal void ExecuteDynamicDelete(object entity)
\r
1258 throw new NotImplementedException();
\r
1262 protected internal void ExecuteDynamicInsert(object entity)
\r
1264 throw new NotImplementedException();
\r
1268 protected internal void ExecuteDynamicUpdate(object entity)
\r
1270 throw new NotImplementedException();
\r