using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Configuration;
using System.Data;
using System.Data.Common;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Transactions;
using System.Xml;
using System.Runtime.CompilerServices;
namespace System.Data.Linq {
using System.Data.Linq.Mapping;
using System.Data.Linq.Provider;
using System.Diagnostics.CodeAnalysis;
///
/// Used to specify how a submit should behave when one
/// or more updates fail due to optimistic concurrency
/// conflicts.
///
public enum ConflictMode {
///
/// Fail immediately when the first change conflict is encountered.
///
FailOnFirstConflict,
///
/// Only fail after all changes have been attempted.
///
ContinueOnConflict
}
///
/// Used to specify a value synchronization strategy.
///
public enum RefreshMode {
///
/// Keep the current values.
///
KeepCurrentValues,
///
/// Current values that have been changed are not modified, but
/// any unchanged values are updated with the current database
/// values. No changes are lost in this merge.
///
KeepChanges,
///
/// All current values are overwritten with current database values,
/// regardless of whether they have been changed.
///
OverwriteCurrentValues
}
///
/// The DataContext is the source of all entities mapped over a database connection.
/// It tracks changes made to all retrieved entities and maintains an 'identity cache'
/// that guarantees that entities retrieved more than once are represented using the
/// same object instance.
///
public class DataContext : IDisposable {
CommonDataServices services;
IProvider provider;
Dictionary tables;
bool objectTrackingEnabled = true;
bool deferredLoadingEnabled = true;
bool disposed;
bool isInSubmitChanges;
DataLoadOptions loadOptions;
ChangeConflictCollection conflicts;
private DataContext() {
}
public DataContext(string fileOrServerOrConnection) {
if (fileOrServerOrConnection == null) {
throw Error.ArgumentNull("fileOrServerOrConnection");
}
this.InitWithDefaultMapping(fileOrServerOrConnection);
}
public DataContext(string fileOrServerOrConnection, MappingSource mapping) {
if (fileOrServerOrConnection == null) {
throw Error.ArgumentNull("fileOrServerOrConnection");
}
if (mapping == null) {
throw Error.ArgumentNull("mapping");
}
this.Init(fileOrServerOrConnection, mapping);
}
public DataContext(IDbConnection connection) {
if (connection == null) {
throw Error.ArgumentNull("connection");
}
this.InitWithDefaultMapping(connection);
}
public DataContext(IDbConnection connection, MappingSource mapping) {
if (connection == null) {
throw Error.ArgumentNull("connection");
}
if (mapping == null) {
throw Error.ArgumentNull("mapping");
}
this.Init(connection, mapping);
}
internal DataContext(DataContext context) {
if (context == null) {
throw Error.ArgumentNull("context");
}
this.Init(context.Connection, context.Mapping.MappingSource);
this.LoadOptions = context.LoadOptions;
this.Transaction = context.Transaction;
this.Log = context.Log;
this.CommandTimeout = context.CommandTimeout;
}
#region Dispose\Finalize
public void Dispose() {
this.disposed = true;
Dispose(true);
// Technically, calling GC.SuppressFinalize is not required because the class does not
// have a finalizer, but it does no harm, protects against the case where a finalizer is added
// in the future, and prevents an FxCop warning.
GC.SuppressFinalize(this);
}
// Not implementing finalizer here because there are no unmanaged resources
// to release. See http://msdnwiki.microsoft.com/en-us/mtpswiki/12afb1ea-3a17-4a3f-a1f0-fcdb853e2359.aspx
// The bulk of the clean-up code is implemented in Dispose(bool)
protected virtual void Dispose(bool disposing) {
// Implemented but empty so that derived contexts can implement
// a finalizer that potentially cleans up unmanaged resources.
if (disposing) {
if (this.provider != null) {
this.provider.Dispose();
this.provider = null;
}
this.services = null;
this.tables = null;
this.loadOptions = null;
}
}
internal void CheckDispose() {
if (this.disposed) {
throw Error.DataContextCannotBeUsedAfterDispose();
}
}
#endregion
private void InitWithDefaultMapping(object connection) {
this.Init(connection, new AttributeMappingSource());
}
internal object Clone() {
CheckDispose();
return Activator.CreateInstance(this.GetType(), new object[] { this.Connection, this.Mapping.MappingSource });
}
private void Init(object connection, MappingSource mapping) {
MetaModel model = mapping.GetModel(this.GetType());
this.services = new CommonDataServices(this, model);
this.conflicts = new ChangeConflictCollection();
// determine provider
Type providerType;
if (model.ProviderType != null) {
providerType = model.ProviderType;
}
else {
throw Error.ProviderTypeNull();
}
if (!typeof(IProvider).IsAssignableFrom(providerType)) {
throw Error.ProviderDoesNotImplementRequiredInterface(providerType, typeof(IProvider));
}
this.provider = (IProvider)Activator.CreateInstance(providerType);
this.provider.Initialize(this.services, connection);
this.tables = new Dictionary();
this.InitTables(this);
}
internal void ClearCache() {
CheckDispose();
this.services.ResetServices();
}
internal CommonDataServices Services {
get {
CheckDispose();
return this.services;
}
}
///
/// The connection object used by this DataContext when executing queries and commands.
///
public DbConnection Connection {
get {
CheckDispose();
return this.provider.Connection;
}
}
///
/// The transaction object used by this DataContext when executing queries and commands.
///
public DbTransaction Transaction {
get {
CheckDispose();
return this.provider.Transaction;
}
set {
CheckDispose();
this.provider.Transaction = value;
}
}
///
/// The command timeout to use when executing commands.
///
public int CommandTimeout {
get {
CheckDispose();
return this.provider.CommandTimeout;
}
set {
CheckDispose();
this.provider.CommandTimeout = value;
}
}
///
/// A text writer used by this DataContext to output information such as query and commands
/// being executed.
///
public TextWriter Log {
get {
CheckDispose();
return this.provider.Log;
}
set {
CheckDispose();
this.provider.Log = value;
}
}
///
/// True if object tracking is enabled, false otherwise. Object tracking
/// includes identity caching and change tracking. If tracking is turned off,
/// SubmitChanges and related functionality is disabled. DeferredLoading is
/// also disabled when object tracking is disabled.
///
public bool ObjectTrackingEnabled {
get {
CheckDispose();
return objectTrackingEnabled;
}
set {
CheckDispose();
if (Services.HasCachedObjects) {
throw Error.OptionsCannotBeModifiedAfterQuery();
}
objectTrackingEnabled = value;
if (!objectTrackingEnabled) {
deferredLoadingEnabled = false;
}
// force reinitialization of cache/tracking objects
services.ResetServices();
}
}
///
/// True if deferred loading is enabled, false otherwise. With deferred
/// loading disabled, association members return default values and are
/// not defer loaded.
///
public bool DeferredLoadingEnabled {
get {
CheckDispose();
return deferredLoadingEnabled;
}
set {
CheckDispose();
if (Services.HasCachedObjects) {
throw Error.OptionsCannotBeModifiedAfterQuery();
}
// can't have tracking disabled and deferred loading enabled
if (!ObjectTrackingEnabled && value) {
throw Error.DeferredLoadingRequiresObjectTracking();
}
deferredLoadingEnabled = value;
}
}
///
/// The mapping model used to describe the entities
///
public MetaModel Mapping {
get {
CheckDispose();
return this.services.Model;
}
}
///
/// Verify that change tracking is enabled, and throw an exception
/// if it is not.
///
internal void VerifyTrackingEnabled() {
CheckDispose();
if (!ObjectTrackingEnabled) {
throw Error.ObjectTrackingRequired();
}
}
///
/// Verify that submit changes is not occurring
///
internal void CheckNotInSubmitChanges() {
CheckDispose();
if (this.isInSubmitChanges) {
throw Error.CannotPerformOperationDuringSubmitChanges();
}
}
///
/// Verify that submit changes is occurring
///
internal void CheckInSubmitChanges() {
CheckDispose();
if (!this.isInSubmitChanges) {
throw Error.CannotPerformOperationOutsideSubmitChanges();
}
}
///
/// Returns the strongly-typed Table object representing a collection of persistent entities.
/// Use this collection as the starting point for queries.
///
/// The type of the entity objects. In case of a persistent hierarchy
/// the entity specified must be the base type of the hierarchy.
///
[SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "[....]: Generic parameters are required for strong-typing of the return type.")]
public Table GetTable() where TEntity : class {
CheckDispose();
MetaTable metaTable = this.services.Model.GetTable(typeof(TEntity));
if (metaTable == null) {
throw Error.TypeIsNotMarkedAsTable(typeof(TEntity));
}
ITable table = this.GetTable(metaTable);
if (table.ElementType != typeof(TEntity)) {
throw Error.CouldNotGetTableForSubtype(typeof(TEntity), metaTable.RowType.Type);
}
return (Table)table;
}
///
/// Returns the weakly-typed ITable object representing a collection of persistent entities.
/// Use this collection as the starting point for dynamic/runtime-computed queries.
///
/// The type of the entity objects. In case of a persistent hierarchy
/// the entity specified must be the base type of the hierarchy.
///
public ITable GetTable(Type type) {
CheckDispose();
if (type == null) {
throw Error.ArgumentNull("type");
}
MetaTable metaTable = this.services.Model.GetTable(type);
if (metaTable == null) {
throw Error.TypeIsNotMarkedAsTable(type);
}
if (metaTable.RowType.Type != type) {
throw Error.CouldNotGetTableForSubtype(type, metaTable.RowType.Type);
}
return this.GetTable(metaTable);
}
private ITable GetTable(MetaTable metaTable) {
System.Diagnostics.Debug.Assert(metaTable != null);
ITable tb;
if (!this.tables.TryGetValue(metaTable, out tb)) {
ValidateTable(metaTable);
Type tbType = typeof(Table<>).MakeGenericType(metaTable.RowType.Type);
tb = (ITable)Activator.CreateInstance(tbType, BindingFlags.Instance|BindingFlags.Public|BindingFlags.NonPublic, null, new object[] { this, metaTable }, null);
this.tables.Add(metaTable, tb);
}
return tb;
}
private static void ValidateTable(MetaTable metaTable) {
// Associations can only be between entities - verify both that both ends of all
// associations are entities.
foreach(MetaAssociation assoc in metaTable.RowType.Associations) {
if(!assoc.ThisMember.DeclaringType.IsEntity) {
throw Error.NonEntityAssociationMapping(assoc.ThisMember.DeclaringType.Type, assoc.ThisMember.Name, assoc.ThisMember.DeclaringType.Type);
}
if(!assoc.OtherType.IsEntity) {
throw Error.NonEntityAssociationMapping(assoc.ThisMember.DeclaringType.Type, assoc.ThisMember.Name, assoc.OtherType.Type);
}
}
}
private void InitTables(object schema) {
FieldInfo[] fields = schema.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance);
foreach (FieldInfo fi in fields) {
Type ft = fi.FieldType;
if (ft.IsGenericType && ft.GetGenericTypeDefinition() == typeof(Table<>)) {
ITable tb = (ITable)fi.GetValue(schema);
if (tb == null) {
Type rowType = ft.GetGenericArguments()[0];
tb = this.GetTable(rowType);
fi.SetValue(schema, tb);
}
}
}
}
///
/// Internal method that can be accessed by tests to retrieve the provider
/// The IProvider result can then be cast to the actual provider to call debug methods like
/// CheckQueries, QueryCount, EnableCacheLookup
///
internal IProvider Provider {
get {
CheckDispose();
return this.provider;
}
}
///
/// Returns true if the database specified by the connection object exists.
///
///
public bool DatabaseExists() {
CheckDispose();
return this.provider.DatabaseExists();
}
///
/// Creates a new database instance (catalog or file) at the location specified by the connection
/// using the metadata encoded within the entities or mapping file.
///
public void CreateDatabase() {
CheckDispose();
this.provider.CreateDatabase();
}
///
/// Deletes the database instance at the location specified by the connection.
///
public void DeleteDatabase() {
CheckDispose();
this.provider.DeleteDatabase();
}
///
/// Submits one or more commands to the database reflecting the changes made to the retreived entities.
/// If a transaction is not already specified one will be created for the duration of this operation.
/// If a change conflict is encountered a ChangeConflictException will be thrown.
///
public void SubmitChanges() {
CheckDispose();
SubmitChanges(ConflictMode.FailOnFirstConflict);
}
///
/// Submits one or more commands to the database reflecting the changes made to the retreived entities.
/// If a transaction is not already specified one will be created for the duration of this operation.
/// If a change conflict is encountered a ChangeConflictException will be thrown.
/// You can override this method to implement common conflict resolution behaviors.
///
/// Determines how SubmitChanges handles conflicts.
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "[....]: In the middle of attempting to rollback a transaction, outer transaction is thrown.")]
public virtual void SubmitChanges(ConflictMode failureMode) {
CheckDispose();
CheckNotInSubmitChanges();
VerifyTrackingEnabled();
this.conflicts.Clear();
try {
this.isInSubmitChanges = true;
if (System.Transactions.Transaction.Current == null && this.provider.Transaction == null) {
bool openedConnection = false;
DbTransaction transaction = null;
try {
if (this.provider.Connection.State == ConnectionState.Open) {
this.provider.ClearConnection();
}
if (this.provider.Connection.State == ConnectionState.Closed) {
this.provider.Connection.Open();
openedConnection = true;
}
transaction = this.provider.Connection.BeginTransaction(IsolationLevel.ReadCommitted);
this.provider.Transaction = transaction;
new ChangeProcessor(this.services, this).SubmitChanges(failureMode);
this.AcceptChanges();
// to commit a transaction, there can be no open readers
// on the connection.
this.provider.ClearConnection();
transaction.Commit();
}
catch {
if (transaction != null) {
transaction.Rollback();
}
throw;
}
finally {
this.provider.Transaction = null;
if (openedConnection) {
this.provider.Connection.Close();
}
}
}
else {
new ChangeProcessor(services, this).SubmitChanges(failureMode);
this.AcceptChanges();
}
}
finally {
this.isInSubmitChanges = false;
}
}
///
/// Refresh the specified object using the mode specified. If the refresh
/// cannot be performed (for example if the object no longer exists in the
/// database) an InvalidOperationException is thrown.
///
/// How the refresh should be performed.
/// The object to refresh. The object must be
/// the result of a previous query.
public void Refresh(RefreshMode mode, object entity)
{
CheckDispose();
CheckNotInSubmitChanges();
VerifyTrackingEnabled();
if (entity == null)
{
throw Error.ArgumentNull("entity");
}
Array items = Array.CreateInstance(entity.GetType(), 1);
items.SetValue(entity, 0);
this.Refresh(mode, items as IEnumerable);
}
///
/// Refresh a set of objects using the mode specified. If the refresh
/// cannot be performed (for example if the object no longer exists in the
/// database) an InvalidOperationException is thrown.
///
/// How the refresh should be performed.
/// The objects to refresh.
public void Refresh(RefreshMode mode, params object[] entities)
{
CheckDispose(); // code hygeine requirement
if (entities == null){
throw Error.ArgumentNull("entities");
}
Refresh(mode, (IEnumerable)entities);
}
///
/// Refresh a collection of objects using the mode specified. If the refresh
/// cannot be performed (for example if the object no longer exists in the
/// database) an InvalidOperationException is thrown.
///
/// How the refresh should be performed.
/// The collection of objects to refresh.
public void Refresh(RefreshMode mode, IEnumerable entities)
{
CheckDispose();
CheckNotInSubmitChanges();
VerifyTrackingEnabled();
if (entities == null) {
throw Error.ArgumentNull("entities");
}
// if the collection is a query, we need to execute and buffer,
// since below we will be issuing additional queries and can only
// have a single reader open.
var list = entities.Cast