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 assemblyName = null;
\r
241 if (!reProvider.IsMatch(connectionString))
\r
243 vendor = "SqlServer";
\r
244 assemblyName = "DbLinq.SqlServer";
\r
248 var match = reProvider.Match(connectionString);
\r
249 vendor = match.Groups[1].Value;
\r
250 assemblyName = "DbLinq." + vendor;
\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 assembly = Assembly.Load(assemblyName);
\r
274 catch (Exception e)
\r
276 throw new ArgumentException(
\r
278 "Unable to load the `{0}' DbLinq vendor within assembly '{1}.dll'.",
\r
279 assemblyName, vendor),
\r
280 "connectionString", e);
\r
284 private void Init(IDatabaseContext databaseContext, MappingSource mappingSource, IVendor vendor)
\r
286 if (databaseContext == null)
\r
287 throw new ArgumentNullException("databaseContext");
\r
289 // Yes, .NET throws an NRE for this. Why it's not ArgumentNullException, I couldn't tell you.
\r
290 if (databaseContext.Connection.ConnectionString == null)
\r
291 throw new NullReferenceException();
\r
293 string connectionString = databaseContext.Connection.ConnectionString;
\r
294 _VendorProvider = ObjectFactory.Get<IVendorProvider>();
\r
295 Vendor = vendor ??
\r
296 (connectionString != null ? GetVendor(ref connectionString) : null) ??
\r
297 _VendorProvider.FindVendorByProviderType(typeof(SqlClient.Sql2005Provider));
\r
299 DatabaseContext = databaseContext;
\r
301 MemberModificationHandler = ObjectFactory.Create<IMemberModificationHandler>(); // not a singleton: object is stateful
\r
302 QueryBuilder = ObjectFactory.Get<IQueryBuilder>();
\r
303 QueryRunner = ObjectFactory.Get<IQueryRunner>();
\r
305 //EntityMap = ObjectFactory.Create<IEntityMap>();
\r
306 identityReaderFactory = ObjectFactory.Get<IIdentityReaderFactory>();
\r
308 _MappingContext = new MappingContext();
\r
310 // initialize the mapping information
\r
311 if (mappingSource == null)
\r
312 mappingSource = new AttributeMappingSource();
\r
313 Mapping = mappingSource.GetModel(GetType());
\r
317 /// Checks if the table is allready mapped or maps it if not.
\r
319 /// <param name="tableType">Type of the table.</param>
\r
320 /// <exception cref="InvalidOperationException">Thrown if the table is not mappable.</exception>
\r
321 private void CheckTableMapping(Type tableType)
\r
323 //This will throw an exception if the table is not found
\r
324 if(Mapping.GetTable(tableType) == null)
\r
326 throw new InvalidOperationException("The type '" + tableType.Name + "' is not mapped as a Table.");
\r
331 /// Returns a Table for the type TEntity.
\r
333 /// <exception cref="InvalidOperationException">If the type TEntity is not mappable as a Table.</exception>
\r
334 /// <typeparam name="TEntity">The table type.</typeparam>
\r
335 public Table<TEntity> GetTable<TEntity>() where TEntity : class
\r
337 return (Table<TEntity>)GetTable(typeof(TEntity));
\r
341 /// Returns a Table for the given type.
\r
343 /// <param name="type">The table type.</param>
\r
344 /// <exception cref="InvalidOperationException">If the type is not mappable as a Table.</exception>
\r
345 public ITable GetTable(Type type)
\r
347 Profiler.At("DataContext.GetTable(typeof({0}))", type != null ? type.Name : null);
\r
348 ITable tableExisting;
\r
349 if (_tableMap.TryGetValue(type, out tableExisting))
\r
350 return tableExisting;
\r
352 //Check for table mapping
\r
353 CheckTableMapping(type);
\r
355 var tableNew = Activator.CreateInstance(
\r
356 typeof(Table<>).MakeGenericType(type)
\r
357 , BindingFlags.NonPublic | BindingFlags.Instance
\r
359 , new object[] { this }
\r
360 , System.Globalization.CultureInfo.CurrentCulture) as ITable;
\r
362 _tableMap[type] = tableNew;
\r
366 public void SubmitChanges()
\r
368 SubmitChanges(ConflictMode.FailOnFirstConflict);
\r
374 /// <returns></returns>
\r
375 public bool DatabaseExists()
\r
379 return Vendor.Ping(this);
\r
388 /// Commits all pending changes to database
\r
390 /// <param name="failureMode"></param>
\r
391 public virtual void SubmitChanges(ConflictMode failureMode)
\r
393 if (this.objectTrackingEnabled == false)
\r
394 throw new InvalidOperationException("Object tracking is not enabled for the current data context instance.");
\r
395 using (DatabaseContext.OpenConnection()) //ConnMgr will close connection for us
\r
397 if (Transaction != null)
\r
398 SubmitChangesImpl(failureMode);
\r
401 using (IDbTransaction transaction = DatabaseContext.CreateTransaction())
\r
405 Transaction = (DbTransaction) transaction;
\r
406 SubmitChangesImpl(failureMode);
\r
407 // TODO: handle conflicts (which can only occur when concurrency mode is implemented)
\r
408 transaction.Commit();
\r
412 Transaction = null;
\r
419 void SubmitChangesImpl(ConflictMode failureMode)
\r
421 var queryContext = new QueryContext(this);
\r
423 // There's no sense in updating an entity when it's going to
\r
424 // be deleted in the current transaction, so do deletes first.
\r
425 foreach (var entityTrack in CurrentTransactionEntities.EnumerateAll().ToList())
\r
427 switch (entityTrack.EntityState)
\r
429 case EntityState.ToDelete:
\r
430 var deleteQuery = QueryBuilder.GetDeleteQuery(entityTrack.Entity, queryContext);
\r
431 QueryRunner.Delete(entityTrack.Entity, deleteQuery);
\r
433 UnregisterDelete(entityTrack.Entity);
\r
434 AllTrackedEntities.RegisterToDelete(entityTrack.Entity);
\r
435 AllTrackedEntities.RegisterDeleted(entityTrack.Entity);
\r
442 foreach (var entityTrack in CurrentTransactionEntities.EnumerateAll()
\r
443 .Concat(AllTrackedEntities.EnumerateAll())
\r
446 switch (entityTrack.EntityState)
\r
448 case EntityState.ToInsert:
\r
449 foreach (var toInsert in GetReferencedObjects(entityTrack.Entity))
\r
451 InsertEntity(toInsert, queryContext);
\r
454 case EntityState.ToWatch:
\r
455 foreach (var toUpdate in GetReferencedObjects(entityTrack.Entity))
\r
457 UpdateEntity(toUpdate, queryContext);
\r
461 throw new ArgumentOutOfRangeException();
\r
466 private IEnumerable<object> GetReferencedObjects(object value)
\r
468 var values = new EntitySet<object>();
\r
469 FillReferencedObjects(value, values);
\r
473 // Breadth-first traversal of an object graph
\r
474 private void FillReferencedObjects(object parent, EntitySet<object> values)
\r
476 if (parent == null)
\r
478 var children = new Queue<object>();
\r
479 children.Enqueue(parent);
\r
480 while (children.Count > 0)
\r
482 object value = children.Dequeue();
\r
484 IEnumerable<MetaAssociation> associationList = Mapping.GetMetaType(value.GetType()).Associations.Where(a => !a.IsForeignKey);
\r
485 if (associationList.Any())
\r
487 foreach (MetaAssociation association in associationList)
\r
489 var memberData = association.ThisMember;
\r
490 var entitySetValue = memberData.Member.GetMemberValue(value);
\r
492 if (entitySetValue != null)
\r
494 var hasLoadedOrAssignedValues = entitySetValue.GetType().GetProperty("HasLoadedOrAssignedValues");
\r
495 if (!((bool)hasLoadedOrAssignedValues.GetValue(entitySetValue, null)))
\r
496 continue; // execution deferred; ignore.
\r
497 foreach (var o in ((IEnumerable)entitySetValue))
\r
498 children.Enqueue(o);
\r
505 private void InsertEntity(object entity, QueryContext queryContext)
\r
507 var insertQuery = QueryBuilder.GetInsertQuery(entity, queryContext);
\r
508 QueryRunner.Insert(entity, insertQuery);
\r
510 UpdateReferencedObjects(entity);
\r
511 MoveToAllTrackedEntities(entity, true);
\r
514 private void UpdateEntity(object entity, QueryContext queryContext)
\r
516 if (!AllTrackedEntities.ContainsReference(entity))
\r
517 InsertEntity(entity, queryContext);
\r
518 else if (MemberModificationHandler.IsModified(entity, Mapping))
\r
520 var modifiedMembers = MemberModificationHandler.GetModifiedProperties(entity, Mapping);
\r
521 var updateQuery = QueryBuilder.GetUpdateQuery(entity, modifiedMembers, queryContext);
\r
522 QueryRunner.Update(entity, updateQuery, modifiedMembers);
\r
524 RegisterUpdateAgain(entity);
\r
525 UpdateReferencedObjects(entity);
\r
526 MoveToAllTrackedEntities(entity, false);
\r
530 private void UpdateReferencedObjects(object root)
\r
532 var metaType = Mapping.GetMetaType(root.GetType());
\r
533 foreach (var assoc in metaType.Associations)
\r
535 var memberData = assoc.ThisMember;
\r
536 //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
537 //Confirmed against default .NET l2sql - association columns are always set, even if AutoSync==AutoSync.Never
\r
538 //if (memberData.Association.ThisKey.Any(m => (m.AutoSync != AutoSync.Always) && (m.AutoSync != sync)))
\r
540 var oks = memberData.Association.OtherKey.Select(m => m.StorageMember).ToList();
\r
541 if (oks.Count == 0)
\r
543 var pks = memberData.Association.ThisKey
\r
544 .Select(m => m.StorageMember.GetMemberValue(root))
\r
546 if (pks.Count != oks.Count)
\r
547 throw new InvalidOperationException(
\r
548 string.Format("Count of primary keys ({0}) doesn't match count of other keys ({1}).",
\r
549 pks.Count, oks.Count));
\r
550 var members = memberData.Member.GetMemberValue(root) as IEnumerable;
\r
551 if (members == null)
\r
553 foreach (var member in members)
\r
555 for (int i = 0; i < pks.Count; ++i)
\r
557 oks[i].SetMemberValue(member, pks[i]);
\r
563 private void MoveToAllTrackedEntities(object entity, bool insert)
\r
565 if (!ObjectTrackingEnabled)
\r
567 if (CurrentTransactionEntities.ContainsReference(entity))
\r
569 CurrentTransactionEntities.RegisterToDelete(entity);
\r
571 CurrentTransactionEntities.RegisterDeleted(entity);
\r
573 if (!AllTrackedEntities.ContainsReference(entity))
\r
575 var identityReader = _GetIdentityReader(entity.GetType());
\r
576 AllTrackedEntities.RegisterToWatch(entity, identityReader.GetIdentityKey(entity));
\r
581 /// TODO - allow generated methods to call into stored procedures
\r
584 internal IExecuteResult _ExecuteMethodCall(DataContext context, System.Reflection.MethodInfo method, params object[] sqlParams)
\r
586 using (DatabaseContext.OpenConnection())
\r
588 System.Data.Linq.IExecuteResult result = Vendor.ExecuteMethodCall(context, method, sqlParams);
\r
594 protected IExecuteResult ExecuteMethodCall(object instance, System.Reflection.MethodInfo methodInfo, params object[] parameters)
\r
596 throw new NotImplementedException();
\r
599 #region Identity management
\r
602 internal IIdentityReader _GetIdentityReader(Type t)
\r
604 IIdentityReader identityReader;
\r
605 if (!identityReaders.TryGetValue(t, out identityReader))
\r
607 identityReader = identityReaderFactory.GetReader(t, this);
\r
608 identityReaders[t] = identityReader;
\r
610 return identityReader;
\r
614 internal object _GetRegisteredEntity(object entity)
\r
616 // TODO: check what is faster: by identity or by ref
\r
617 var identityReader = _GetIdentityReader(entity.GetType());
\r
618 var identityKey = identityReader.GetIdentityKey(entity);
\r
619 if (identityKey == null) // if we don't have an entitykey here, it means that the entity has no PK
\r
622 var registeredEntityTrack =
\r
623 CurrentTransactionEntities.FindByIdentity(identityKey) ??
\r
624 AllTrackedEntities.FindByIdentity(identityKey);
\r
625 if (registeredEntityTrack != null)
\r
626 return registeredEntityTrack.Entity;
\r
630 //internal object GetRegisteredEntityByKey(IdentityKey identityKey)
\r
632 // return EntityMap[identityKey];
\r
636 /// Registers an entity in a watch state
\r
638 /// <param name="entity"></param>
\r
639 /// <returns></returns>
\r
641 internal object _GetOrRegisterEntity(object entity)
\r
643 var identityReader = _GetIdentityReader(entity.GetType());
\r
644 var identityKey = identityReader.GetIdentityKey(entity);
\r
645 SetEntitySetsQueries(entity);
\r
646 SetEntityRefQueries(entity);
\r
648 // if we have no identity, we can't track it
\r
649 if (identityKey == null)
\r
652 // try to find an already registered entity and return it
\r
653 var registeredEntityTrack =
\r
654 CurrentTransactionEntities.FindByIdentity(identityKey) ??
\r
655 AllTrackedEntities.FindByIdentity(identityKey);
\r
656 if (registeredEntityTrack != null)
\r
657 return registeredEntityTrack.Entity;
\r
659 // otherwise, register and return
\r
660 AllTrackedEntities.RegisterToWatch(entity, identityKey);
\r
664 readonly IDataMapper DataMapper = ObjectFactory.Get<IDataMapper>();
\r
665 private void SetEntityRefQueries(object entity)
\r
667 if (!this.deferredLoadingEnabled)
\r
670 // BUG: This is ignoring External Mappings from XmlMappingSource.
\r
672 Type thisType = entity.GetType();
\r
673 IEnumerable<MetaAssociation> associationList = Mapping.GetMetaType(entity.GetType()).Associations.Where(a => a.IsForeignKey);
\r
674 foreach (MetaAssociation association in associationList)
\r
676 //example of entityRef:Order.Employee
\r
677 var memberData = association.ThisMember;
\r
678 Type otherTableType = association.OtherType.Type;
\r
679 ParameterExpression p = Expression.Parameter(otherTableType, "other");
\r
681 var otherTable = GetTable(otherTableType);
\r
683 //ie:EmployeeTerritories.EmployeeID
\r
684 var foreignKeys = memberData.Association.ThisKey;
\r
685 BinaryExpression predicate = null;
\r
686 var otherPKs = memberData.Association.OtherKey;
\r
687 IEnumerator<MetaDataMember> otherPKEnumerator = otherPKs.GetEnumerator();
\r
689 if (otherPKs.Count != foreignKeys.Count)
\r
690 throw new InvalidOperationException("Foreign keys don't match ThisKey");
\r
691 foreach (MetaDataMember key in foreignKeys)
\r
693 otherPKEnumerator.MoveNext();
\r
695 var thisForeignKeyProperty = (PropertyInfo)key.Member;
\r
696 object thisForeignKeyValue = thisForeignKeyProperty.GetValue(entity, null);
\r
698 if (thisForeignKeyValue != null)
\r
700 BinaryExpression keyPredicate;
\r
701 if (!(thisForeignKeyProperty.PropertyType.IsNullable()))
\r
703 keyPredicate = Expression.Equal(Expression.MakeMemberAccess(p, otherPKEnumerator.Current.Member),
\r
704 Expression.Constant(thisForeignKeyValue));
\r
708 var ValueProperty = thisForeignKeyProperty.PropertyType.GetProperty("Value");
\r
709 keyPredicate = Expression.Equal(Expression.MakeMemberAccess(p, otherPKEnumerator.Current.Member),
\r
710 Expression.Constant(ValueProperty.GetValue(thisForeignKeyValue, null)));
\r
713 if (predicate == null)
\r
714 predicate = keyPredicate;
\r
716 predicate = Expression.And(predicate, keyPredicate);
\r
719 IEnumerable query = null;
\r
720 if (predicate != null)
\r
722 query = GetOtherTableQuery(predicate, p, otherTableType, otherTable) as IEnumerable;
\r
723 //it would be interesting surround the above query with a .Take(1) expression for performance.
\r
726 // If no separate Storage is specified, use the member directly
\r
727 MemberInfo storage = memberData.StorageMember;
\r
728 if (storage == null)
\r
729 storage = memberData.Member;
\r
731 // Check that the storage is a field or a writable property
\r
732 if (!(storage is FieldInfo) && !(storage is PropertyInfo && ((PropertyInfo)storage).CanWrite)) {
\r
733 throw new InvalidOperationException(String.Format(
\r
734 "Member {0}.{1} is not a field nor a writable property",
\r
735 storage.DeclaringType, storage.Name));
\r
738 Type storageType = storage.GetMemberType();
\r
740 object entityRefValue = null;
\r
742 entityRefValue = Activator.CreateInstance(storageType, query);
\r
744 entityRefValue = Activator.CreateInstance(storageType);
\r
746 storage.SetMemberValue(entity, entityRefValue);
\r
751 /// 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
752 /// Here we set the query source of each EntitySetProperty
\r
754 /// <param name="entity"></param>
\r
755 private void SetEntitySetsQueries(object entity)
\r
757 if (!this.deferredLoadingEnabled)
\r
760 // BUG: This is ignoring External Mappings from XmlMappingSource.
\r
762 IEnumerable<MetaAssociation> associationList = Mapping.GetMetaType(entity.GetType()).Associations.Where(a => !a.IsForeignKey);
\r
764 if (associationList.Any())
\r
766 foreach (MetaAssociation association in associationList)
\r
768 //example of entitySet: Employee.EmployeeTerritories
\r
769 var memberData = association.ThisMember;
\r
770 Type otherTableType = association.OtherType.Type;
\r
771 ParameterExpression p = Expression.Parameter(otherTableType, "other");
\r
773 //other table:EmployeeTerritories
\r
774 var otherTable = GetTable(otherTableType);
\r
776 var otherKeys = memberData.Association.OtherKey;
\r
777 var thisKeys = memberData.Association.ThisKey;
\r
778 if (otherKeys.Count != thisKeys.Count)
\r
779 throw new InvalidOperationException("This keys don't match OtherKey");
\r
780 BinaryExpression predicate = null;
\r
781 IEnumerator<MetaDataMember> thisKeyEnumerator = thisKeys.GetEnumerator();
\r
782 foreach (MetaDataMember otherKey in otherKeys)
\r
784 thisKeyEnumerator.MoveNext();
\r
785 //other table member:EmployeeTerritories.EmployeeID
\r
786 var otherTableMember = (PropertyInfo)otherKey.Member;
\r
788 BinaryExpression keyPredicate;
\r
789 if (!(otherTableMember.PropertyType.IsNullable()))
\r
791 keyPredicate = Expression.Equal(Expression.MakeMemberAccess(p, otherTableMember),
\r
792 Expression.Constant(thisKeyEnumerator.Current.Member.GetMemberValue(entity)));
\r
796 var ValueProperty = otherTableMember.PropertyType.GetProperty("Value");
\r
797 keyPredicate = Expression.Equal(Expression.MakeMemberAccess(
\r
798 Expression.MakeMemberAccess(p, otherTableMember),
\r
800 Expression.Constant(thisKeyEnumerator.Current.Member.GetMemberValue(entity)));
\r
802 if (predicate == null)
\r
803 predicate = keyPredicate;
\r
805 predicate = Expression.And(predicate, keyPredicate);
\r
808 var query = GetOtherTableQuery(predicate, p, otherTableType, otherTable);
\r
810 var entitySetValue = memberData.Member.GetMemberValue(entity);
\r
812 if (entitySetValue == null)
\r
814 entitySetValue = Activator.CreateInstance(memberData.Member.GetMemberType());
\r
815 memberData.Member.SetMemberValue(entity, entitySetValue);
\r
818 var hasLoadedOrAssignedValues = entitySetValue.GetType().GetProperty("HasLoadedOrAssignedValues");
\r
819 if ((bool)hasLoadedOrAssignedValues.GetValue(entitySetValue, null))
\r
822 var setSourceMethod = entitySetValue.GetType().GetMethod("SetSource");
\r
823 setSourceMethod.Invoke(entitySetValue, new[] { query });
\r
824 //employee.EmployeeTerritories.SetSource(Table[EmployeesTerritories].Where(other=>other.employeeID="WARTH"))
\r
829 private static MethodInfo _WhereMethod = typeof(Queryable).GetMethods().First(m => m.Name == "Where");
\r
830 internal object GetOtherTableQuery(Expression predicate, ParameterExpression parameter, Type otherTableType, IQueryable otherTable)
\r
832 //predicate: other.EmployeeID== "WARTH"
\r
833 Expression lambdaPredicate = Expression.Lambda(predicate, parameter);
\r
834 //lambdaPredicate: other=>other.EmployeeID== "WARTH"
\r
836 Expression call = Expression.Call(_WhereMethod.MakeGenericMethod(otherTableType), otherTable.Expression, lambdaPredicate);
\r
837 //Table[EmployeesTerritories].Where(other=>other.employeeID="WARTH")
\r
839 return otherTable.Provider.CreateQuery(call);
\r
844 #region Insert/Update/Delete management
\r
847 /// Registers an entity for insert
\r
849 /// <param name="entity"></param>
\r
850 internal void RegisterInsert(object entity)
\r
852 CurrentTransactionEntities.RegisterToInsert(entity);
\r
855 private void DoRegisterUpdate(object entity)
\r
857 if (entity == null)
\r
858 throw new ArgumentNullException("entity");
\r
860 if (!this.objectTrackingEnabled)
\r
863 var identityReader = _GetIdentityReader(entity.GetType());
\r
864 var identityKey = identityReader.GetIdentityKey(entity);
\r
865 // if we have no key, we can not watch
\r
866 if (identityKey == null || identityKey.Keys.Count == 0)
\r
869 AllTrackedEntities.RegisterToWatch(entity, identityKey);
\r
873 /// Registers an entity for update
\r
874 /// The entity will be updated only if some of its members have changed after the registration
\r
876 /// <param name="entity"></param>
\r
877 internal void RegisterUpdate(object entity)
\r
879 DoRegisterUpdate(entity);
\r
880 MemberModificationHandler.Register(entity, Mapping);
\r
884 /// Registers or re-registers an entity and clears its state
\r
886 /// <param name="entity"></param>
\r
887 /// <returns></returns>
\r
888 internal object Register(object entity)
\r
890 if (! this.objectTrackingEnabled)
\r
892 var registeredEntity = _GetOrRegisterEntity(entity);
\r
893 // the fact of registering again clears the modified state, so we're... clear with that
\r
894 MemberModificationHandler.Register(registeredEntity, Mapping);
\r
895 return registeredEntity;
\r
899 /// Registers an entity for update
\r
900 /// The entity will be updated only if some of its members have changed after the registration
\r
902 /// <param name="entity"></param>
\r
903 /// <param name="entityOriginalState"></param>
\r
904 internal void RegisterUpdate(object entity, object entityOriginalState)
\r
906 if (!this.objectTrackingEnabled)
\r
908 DoRegisterUpdate(entity);
\r
909 MemberModificationHandler.Register(entity, entityOriginalState, Mapping);
\r
913 /// Clears the current state, and marks the object as clean
\r
915 /// <param name="entity"></param>
\r
916 internal void RegisterUpdateAgain(object entity)
\r
918 if (!this.objectTrackingEnabled)
\r
920 MemberModificationHandler.ClearModified(entity, Mapping);
\r
924 /// Registers an entity for delete
\r
926 /// <param name="entity"></param>
\r
927 internal void RegisterDelete(object entity)
\r
929 if (!this.objectTrackingEnabled)
\r
931 CurrentTransactionEntities.RegisterToDelete(entity);
\r
935 /// Unregisters entity after deletion
\r
937 /// <param name="entity"></param>
\r
938 internal void UnregisterDelete(object entity)
\r
940 if (!this.objectTrackingEnabled)
\r
942 CurrentTransactionEntities.RegisterDeleted(entity);
\r
948 /// Changed object determine
\r
950 /// <returns>Lists of inserted, updated, deleted objects</returns>
\r
951 public ChangeSet GetChangeSet()
\r
953 var inserts = new List<object>();
\r
954 var updates = new List<object>();
\r
955 var deletes = new List<object>();
\r
956 foreach (var entityTrack in CurrentTransactionEntities.EnumerateAll()
\r
957 .Concat(AllTrackedEntities.EnumerateAll()))
\r
959 switch (entityTrack.EntityState)
\r
961 case EntityState.ToInsert:
\r
962 inserts.Add(entityTrack.Entity);
\r
964 case EntityState.ToWatch:
\r
965 if (MemberModificationHandler.IsModified(entityTrack.Entity, Mapping))
\r
966 updates.Add(entityTrack.Entity);
\r
968 case EntityState.ToDelete:
\r
969 deletes.Add(entityTrack.Entity);
\r
972 throw new ArgumentOutOfRangeException();
\r
975 return new ChangeSet(inserts, updates, deletes);
\r
979 /// use ExecuteCommand to call raw SQL
\r
981 public int ExecuteCommand(string command, params object[] parameters)
\r
983 var directQuery = QueryBuilder.GetDirectQuery(command, new QueryContext(this));
\r
984 return QueryRunner.Execute(directQuery, parameters);
\r
988 /// Execute raw SQL query and return object
\r
990 public IEnumerable<TResult> ExecuteQuery<TResult>(string query, params object[] parameters) where TResult : new()
\r
993 throw new ArgumentNullException("query");
\r
995 return CreateExecuteQueryEnumerable<TResult>(query, parameters);
\r
998 private IEnumerable<TResult> CreateExecuteQueryEnumerable<TResult>(string query, object[] parameters)
\r
999 where TResult : new()
\r
1001 foreach (TResult result in ExecuteQuery(typeof(TResult), query, parameters))
\r
1002 yield return result;
\r
1005 public IEnumerable ExecuteQuery(Type elementType, string query, params object[] parameters)
\r
1007 if (elementType == null)
\r
1008 throw new ArgumentNullException("elementType");
\r
1009 if (query == null)
\r
1010 throw new ArgumentNullException("query");
\r
1012 var queryContext = new QueryContext(this);
\r
1013 var directQuery = QueryBuilder.GetDirectQuery(query, queryContext);
\r
1014 return QueryRunner.ExecuteSelect(elementType, directQuery, parameters);
\r
1018 /// Gets or sets the load options
\r
1021 public DataLoadOptions LoadOptions
\r
1023 get { throw new NotImplementedException(); }
\r
1024 set { throw new NotImplementedException(); }
\r
1027 public DbTransaction Transaction {
\r
1028 get { return (DbTransaction) DatabaseContext.CurrentTransaction; }
\r
1029 set { DatabaseContext.CurrentTransaction = value; }
\r
1033 /// Runs the given reader and returns columns.
\r
1035 /// <typeparam name="TResult">The type of the result.</typeparam>
\r
1036 /// <param name="reader">The reader.</param>
\r
1037 /// <returns></returns>
\r
1038 public IEnumerable<TResult> Translate<TResult>(DbDataReader reader)
\r
1040 if (reader == null)
\r
1041 throw new ArgumentNullException("reader");
\r
1042 return CreateTranslateIterator<TResult>(reader);
\r
1045 IEnumerable<TResult> CreateTranslateIterator<TResult>(DbDataReader reader)
\r
1047 foreach (TResult result in Translate(typeof(TResult), reader))
\r
1048 yield return result;
\r
1051 public IMultipleResults Translate(DbDataReader reader)
\r
1053 throw new NotImplementedException();
\r
1056 public IEnumerable Translate(Type elementType, DbDataReader reader)
\r
1058 if (elementType == null)
\r
1059 throw new ArgumentNullException("elementType");
\r
1060 if (reader == null)
\r
1061 throw new ArgumentNullException("reader");
\r
1063 return QueryRunner.EnumerateResult(elementType, reader, this);
\r
1066 public void Dispose()
\r
1068 //connection closing should not be done here.
\r
1069 //read: http://msdn2.microsoft.com/en-us/library/bb292288.aspx
\r
1071 //We own the instance of MemberModificationHandler - we must unregister listeners of entities we attached to
\r
1072 MemberModificationHandler.UnregisterAll();
\r
1076 protected virtual void Dispose(bool disposing)
\r
1078 throw new NotImplementedException();
\r
1082 /// Creates a IDbDataAdapter. Used internally by Vendors
\r
1084 /// <returns></returns>
\r
1085 internal IDbDataAdapter CreateDataAdapter()
\r
1087 return DatabaseContext.CreateDataAdapter();
\r
1091 /// Sets a TextWriter where generated SQL commands are written
\r
1093 public TextWriter Log { get; set; }
\r
1096 /// Writes text on Log (if not null)
\r
1097 /// Internal helper
\r
1099 /// <param name="text"></param>
\r
1100 internal void WriteLog(string text)
\r
1103 Log.WriteLine(text);
\r
1107 /// Write an IDbCommand to Log (if non null)
\r
1109 /// <param name="command"></param>
\r
1110 internal void WriteLog(IDbCommand command)
\r
1114 Log.WriteLine(command.CommandText);
\r
1115 foreach (IDbDataParameter parameter in command.Parameters)
\r
1116 WriteLog(parameter);
\r
1118 Log.Write(" Context: {0}", Vendor.VendorName);
\r
1119 Log.Write(" Model: {0}", Mapping.GetType().Name);
\r
1120 Log.Write(" Build: {0}", Assembly.GetExecutingAssembly().GetName().Version);
\r
1126 /// Writes and IDbDataParameter to Log (if non null)
\r
1128 /// <param name="parameter"></param>
\r
1129 internal void WriteLog(IDbDataParameter parameter)
\r
1133 // -- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [2]
\r
1134 // -- <name>: <direction> <type> (...) [<value>]
\r
1135 Log.WriteLine("-- {0}: {1} {2} (Size = {3}; Prec = {4}; Scale = {5}) [{6}]",
\r
1136 parameter.ParameterName, parameter.Direction, parameter.DbType,
\r
1137 parameter.Size, parameter.Precision, parameter.Scale, parameter.Value);
\r
1141 public bool ObjectTrackingEnabled
\r
1143 get { return this.objectTrackingEnabled; }
\r
1146 if (this.currentTransactionEntities != null && value != this.objectTrackingEnabled)
\r
1147 throw new InvalidOperationException("Data context options cannot be modified after results have been returned from a query.");
\r
1148 this.objectTrackingEnabled = value;
\r
1153 public int CommandTimeout
\r
1155 get { throw new NotImplementedException(); }
\r
1156 set { throw new NotImplementedException(); }
\r
1159 public bool DeferredLoadingEnabled
\r
1161 get { return this.deferredLoadingEnabled; }
\r
1164 if (this.currentTransactionEntities != null && value != this.deferredLoadingEnabled)
\r
1165 throw new InvalidOperationException("Data context options cannot be modified after results have been returned from a query.");
\r
1166 this.deferredLoadingEnabled = value;
\r
1171 public ChangeConflictCollection ChangeConflicts
\r
1173 get { throw new NotImplementedException(); }
\r
1177 public DbCommand GetCommand(IQueryable query)
\r
1179 DbCommand dbCommand = GetIDbCommand(query) as DbCommand;
\r
1180 if (dbCommand == null)
\r
1181 throw new InvalidOperationException();
\r
1187 public IDbCommand GetIDbCommand(IQueryable query)
\r
1189 if (query == null)
\r
1190 throw new ArgumentNullException("query");
\r
1192 var qp = query.Provider as QueryProvider;
\r
1194 throw new InvalidOperationException();
\r
1196 if (qp.ExpressionChain.Expressions.Count == 0)
\r
1197 qp.ExpressionChain.Expressions.Add(CreateDefaultQuery(query));
\r
1199 return qp.GetQuery(null).GetCommand().Command;
\r
1202 private Expression CreateDefaultQuery(IQueryable query)
\r
1204 // Manually create the expression tree for: IQueryable<TableType>.Select(e => e)
\r
1205 var identityParameter = Expression.Parameter(query.ElementType, "e");
\r
1206 var identityBody = Expression.Lambda(
\r
1207 typeof(Func<,>).MakeGenericType(query.ElementType, query.ElementType),
\r
1208 identityParameter,
\r
1209 new[] { identityParameter }
\r
1212 return Expression.Call(
\r
1213 typeof(Queryable),
\r
1215 new[] { query.ElementType, query.ElementType },
\r
1217 Expression.Quote(identityBody)
\r
1222 public void Refresh(RefreshMode mode, IEnumerable entities)
\r
1224 throw new NotImplementedException();
\r
1228 public void Refresh(RefreshMode mode, params object[] entities)
\r
1230 throw new NotImplementedException();
\r
1234 public void Refresh(RefreshMode mode, object entity)
\r
1236 throw new NotImplementedException();
\r
1240 public void DeleteDatabase()
\r
1242 throw new NotImplementedException();
\r
1246 public void CreateDatabase()
\r
1248 throw new NotImplementedException();
\r
1252 protected internal IQueryable<TResult> CreateMethodCallQuery<TResult>(object instance, MethodInfo methodInfo, params object[] parameters)
\r
1254 throw new NotImplementedException();
\r
1258 protected internal void ExecuteDynamicDelete(object entity)
\r
1260 throw new NotImplementedException();
\r
1264 protected internal void ExecuteDynamicInsert(object entity)
\r
1266 throw new NotImplementedException();
\r
1270 protected internal void ExecuteDynamicUpdate(object entity)
\r
1272 throw new NotImplementedException();
\r