/*
* Olbert.Utilities.nHydrate
* utilities for simplifying the use of nHydrate entities in WPF applications
* Copyright (C) 2011 Mark A. Olbert
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.ObjectModel;
using Olbert.Utilities;
using System.Data.Objects;
using System.Runtime.Serialization;
using System.Configuration;
using System.Xml.Serialization;
using System.Xml.Linq;
using System.Reflection;
using System.Xml;
namespace Olbert.nHydrate
{
/// <summary>
/// Defines a class which holds information about an individual nHydrate EFDAL "connection", i.e., the
/// information needed to define an EntityConnectionString. Implemented as a KeyedCollection of
/// DatabaseConnectionInfo objects, each of which defines the Sql connection parameters for a database
/// that will work with the required nHydrate schema and version
/// </summary>
[Serializable]
public class nHydrateConnection : ObservableCollection<DatabaseConnectionInfo>,
IXmlSerializable, IDatabaseInfoCollection
{
private string name;
private Type entityContextType;
private string provider;
private Guid schema;
private nHydrateVersion version;
private int defaultDBIndex = -1;
/// <summary>
/// Initializes an instance from the supplied values and sets Provider to its
/// default value, System.Data.SqlClient
/// </summary>
/// <param name="name">the name of the instance being created. Null or empty value will
/// cause an exception</param>
/// <param name="entityContextType">the Type of EntityContext/ObjectContext being described.
/// Types other than ObjectContext itself or a class derived from ObjectContext will cause
/// an exception. A Type that does not have a public constructor taking a single
/// string argument (i.e., an entity connection string) will also cause an exception.</param>
/// <param name="schema">the nHydrate schema. Empty or null values will cause an exception</param>
/// <param name="version">the nHydrate schmea version. Undefined values will cause an
/// exception</param>
public nHydrateConnection( string name, Type entityContextType, Guid schema, nHydrateVersion version )
: this()
{
Name = name;
EntityContextType = entityContextType;
Schema = schema;
Version = version;
}
/// <summary>
/// Initializes an instance and sets Provider to its default value, System.Data.SqlClient. This
/// variant is intended to be called by the Xml configuration file parser
/// </summary>
protected internal nHydrateConnection()
{
Provider = "System.Data.SqlClient";
}
/// <summary>
/// The name of the DatabaseConnection (which is also the key). Cannot be null or
/// empty. Empty or null values throw an exception.
/// </summary>
public string Name
{
get { return name; }
protected set
{
if( ( value == null ) || String.IsNullOrEmpty(value.Trim()) )
throw new TracedException("Supplied nHydrateConnection Name is undefined or empty");
name = value;
}
}
/// <summary>
/// Gets or sets the index in the collection of the default DatabaseConnectionInfo object.
/// Values < -1 or out of bounds are coerced to -1, which is the "no default exists" value
/// </summary>
public int DefaultDatabaseIndex
{
get { return defaultDBIndex; }
set
{
if( value < -1 ) value = -1;
if( value >= Count ) value = -1;
defaultDBIndex = value;
}
}
/// <summary>
/// Gets the default DatabaseConnectionInfo object if one is defined, returning null
/// if it isn't
/// </summary>
public DatabaseConnectionInfo DefaultDatabase
{
get
{
if( ( DefaultDatabaseIndex < 0 ) || ( DefaultDatabaseIndex >= Count ) ) return null;
return this[DefaultDatabaseIndex];
}
}
/// <summary>
/// Gets or sets the Type of the entity context object being represented by this instance.
/// Null values, or values that are neither ObjectContext nor descended from ObjectContext
/// will cause an exception to be thrown.
/// <para>The provided Type must also have a public constructor that takes a single string
/// parameter. The nHydrate code generator produces such a variant, which can be used to
/// create an entity context instance from a particular connection string. That
/// functionality is required for this nHydrate utility framework to work.</para>
/// </summary>
public Type EntityContextType
{
get { return entityContextType; }
set
{
if( value == null )
throw new TracedException("Supplied EntityContext Type was undefined");
if( !( value == typeof(ObjectContext) )
&& !value.IsSubclassOf(typeof(ObjectContext)) )
throw new TracedException("Supplied EntityContext Type was not an ObjectContext or derived from ObjectContext");
ConstructorInfo ctor = value.GetConstructor(new Type[] { typeof(string) });
if( ctor == null )
throw new TracedException("Supplied EntityContext Type does not have a public constructor taking a single string argument");
entityContextType = value;
EntityConstructor = ctor;
}
}
/// <summary>
/// Gets or sets the database provider used by the Entity Framework. Null or empty values are
/// not allowed, and will produce an exception. Default is System.Data.SqlClient.
/// </summary>
public string Provider
{
get { return provider; }
set
{
if( ( value == null ) || String.IsNullOrEmpty(value.Trim()) )
throw new TracedException("Supplied Provider value was undefined or empty");
provider = value;
}
}
/// <summary>
/// Gets or sets the nHydrate schema that all the database connections defined by this instance
/// must have
/// </summary>
public Guid Schema
{
get { return schema; }
protected set
{
if( ( value == null ) || ( value == Guid.Empty ) )
throw new TracedException("Supplied nHydrate schema is undefined or empty");
schema = value;
}
}
/// <summary>
/// Gets or sets the version of the nHydrate schema that all the database connections defined by
/// this instance must have
/// </summary>
public nHydrateVersion Version
{
get { return version; }
protected set
{
if( ( value == null ) || value.IsFirstUpgrade || value.IsUndefined )
throw new TracedException("Supplied nHydrate version is undefined");
version = value;
}
}
/// <summary>
/// Gets an enumerator which returns all of the databases contained in this instance which are
/// compatible with it.
/// <para>Because this method accesses each SqlServer database it can take a long time to
/// complete if there is a problem connecting to a database.</para>
/// </summary>
/// <param name="a">the Rijndael salt used to encrypt user credentials</param>
/// <param name="b">the Rijndal key used to encrypt user credentials</param>
/// <returns>an enumerator which returns all of the databases contained in this instance which are
/// compatible with it</returns>
public IEnumerator<DatabaseConnectionInfo> GetCompatibleDatabases( string a, string b )
{
foreach( DatabaseConnectionInfo curDCI in this )
{
if( curDCI.IsCompatibleWithSchemaAndVersion(a, b) )
yield return curDCI;
}
yield break;
}
/// <summary>
/// Deletes any DatabaseConnectionInfo objects in this instance which are not compatible with
/// the instance's nHydrate schema and version.
/// <para>Because this method accesses each SqlServer database it can take a long time to
/// complete if there is a problem connecting to a database.</para>
/// <para>Be careful using this method. If you pass it the wrong Rijndael salt and key it will
/// delete every DatabaseConnectionInfo object which does not use integrated security (i.e.,
/// Windows identity based authentication) for access.</para>
/// </summary>
/// <param name="a">the Rijndael salt used to encrypt user credentials</param>
/// <param name="b">the Rijndal key used to encrypt user credentials</param>
public void RemoveIncompatibleDatabases( string a, string b )
{
List<int> toRemove = new List<int>();
for( int idx = 0; idx < Count; idx++ )
{
if( !this[idx].IsCompatibleWithSchemaAndVersion(a, b) )
toRemove.Add(idx);
}
for( int idx = toRemove.Count - 1; idx >= 0; idx-- )
{
this.RemoveAt(toRemove[idx]);
}
}
/// <summary>
/// Removes the DatabaseConnectionInfo object with the supplied Name, if it
/// exists in the collection
/// </summary>
/// <param name="dbConnectionName">the Name of the DatabaseConnectionInfo object
/// to be deleted</param>
/// <returns>true if the item was found and deleted, false otherwise</returns>
public bool Remove( string dbConnectionName )
{
int toRemove = -1;
for( int idx = 0; idx < Count; idx++ )
{
if( this[idx].Name == dbConnectionName )
{
toRemove = idx;
break;
}
}
if( toRemove < 0 ) return false;
RemoveAt(toRemove);
return true;
}
/// <summary>
/// Gets the EntityContext's constructor which takes a single string argument, an
/// EntityConnectionString
/// </summary>
public ConstructorInfo EntityConstructor { get; protected set; }
/// <summary>
/// Part of the IXmlSerializable interface. This implementation always returns null;
/// <para>Implementing the IXmlSerializable interface is difficult. I am indebted to
/// Jaap de Haan's article on CodeProject, which explained the process in great detail,
/// and which shows how to work around the numerous gotchas. You can read Jaap's
/// article at http://www.codeproject.com/KB/XML/ImplementIXmlSerializable.aspx.</para>
/// </summary>
/// <returns>null</returns>
public System.Xml.Schema.XmlSchema GetSchema()
{
return null;
}
/// <summary>
/// Part of the IXmlSerializable interface. This implementation reads the Xml sourced from
/// the configuration file and parses it into an nHydrateConnection object. It would be quite
/// odd to call this method from your own code. It is intended to be called by the .NET
/// Framework's configuration subsystem.
/// <para>Implementing the IXmlSerializable interface is difficult. I am indebted to
/// Jaap de Haan's article on CodeProject, which explained the process in great detail,
/// and which shows how to work around the numerous gotchas. You can read Jaap's
/// article at http://www.codeproject.com/KB/XML/ImplementIXmlSerializable.aspx.</para>
/// </summary>
/// <param name="reader">An XmlReader provided by the .NET Framework configuration subsystem</param>
public void ReadXml( System.Xml.XmlReader reader )
{
// unfortunately, you can't use linq to xml here, apparently because the reader
// isn't starting at the beginning of the document or something. so we have to
// do this the old-fashioned way...
//reader.MoveToContent();
Name = reader.GetAttribute("Name");
EntityContextType = Type.GetType(reader.GetAttribute("EntityContextType"));
// read, but do not set, the default database index until after we
// load all the DatabaseConnectionInfo objects. This is necessary because
// otherwise the default index will get "bumped" when the first DatabaseConnectionInfo
// object is added, since its index, 0, will always be less than or equal to
// any default index value
int defaultDB = -1;
int.TryParse(reader.GetAttribute("DefaultDatabaseIndex"), out defaultDB);
string temp = reader.GetAttribute("Provider");
if( ( temp != null ) && !String.IsNullOrEmpty(temp.Trim()) )
Provider = temp;
Guid tempGuid = Guid.Empty;
Guid.TryParse(reader.GetAttribute("Schema"), out tempGuid);
Schema = tempGuid;
Version = nHydrateVersion.Parse(reader.GetAttribute("Version"));
// we need to consume the element or the deserialization routine will call
// ReadXml endlessly
bool emptyElement = reader.IsEmptyElement;
reader.ReadStartElement();
if( !emptyElement )
{
int numConn = 0;
int.TryParse(reader.GetAttribute("Count"), out numConn);
for( int idx = 0; idx < numConn; idx++ )
{
DatabaseConnectionInfo dbci = new DatabaseConnectionInfo(this);
reader.ReadStartElement();
dbci.ReadXml(reader);
this.Add(dbci);
}
}
// now we can finally set whatever default database index was stored
DefaultDatabaseIndex = defaultDB;
}
/// <summary>
/// Part of the IXmlSerializable interface. This implementation writes an Xml representation
/// of the instance to the supplied XmlWriter. It would be quite
/// odd to call this method from your own code. It is intended to be called by the .NET
/// Framework's configuration subsystem.
/// <para>Implementing the IXmlSerializable interface is difficult. I am indebted to
/// Jaap de Haan's article on CodeProject, which explained the process in great detail,
/// and which shows how to work around the numerous gotchas. You can read Jaap's
/// article at http://www.codeproject.com/KB/XML/ImplementIXmlSerializable.aspx.</para>
/// </summary>
/// <param name="writer">An XmlWriter provided by the .NET Framework configuration subsystem</param>
public void WriteXml( System.Xml.XmlWriter writer )
{
writer.WriteAttributeString("Name", Name);
writer.WriteAttributeString("EntityContextType", EntityContextType.AssemblyQualifiedName);
if( DefaultDatabaseIndex >= 0 )
writer.WriteAttributeString("DefaultDatabaseIndex", DefaultDatabaseIndex.ToString());
if( (Schema != null) && (Schema != Guid.Empty) )
writer.WriteAttributeString("Schema", Schema.ToString());
if( ( Version != null ) && !Version.IsUndefined && !Version.IsFirstUpgrade )
writer.WriteAttributeString("Version", Version.ToString());
if( Count > 0 )
{
writer.WriteStartElement("ArrayOfDatabaseConnectionInfo");
writer.WriteAttributeString("Count", Count.ToString());
foreach( DatabaseConnectionInfo curDCI in this )
{
writer.WriteStartElement("DatabaseConnectionInfo");
curDCI.WriteXml(writer);
writer.WriteEndElement();
}
writer.WriteEndElement();
}
}
/// <summary>
/// Overrides the base implementation to remove the association between the items
/// being removed and this object (i.e., sets the List property of each
/// DatabaseConnectionInfo object to null)
/// </summary>
protected override void ClearItems()
{
// remove the association with this object
foreach( DatabaseConnectionInfo curDBCI in this )
{
curDBCI.List = null;
}
DefaultDatabaseIndex = -1;
base.ClearItems();
}
/// <summary>
/// Overrides the base implementation to associate the item being added with
/// this object (i.e., sets the List property of the item being inserted to
/// this instance)
/// </summary>
/// <param name="index">the location in the collection where the object is being inserted</param>
/// <param name="item">the DatabaseConnectionInfo item being inserted</param>
protected override void InsertItem( int index, DatabaseConnectionInfo item )
{
item.List = this;
base.InsertItem(index, item);
if( index <= DefaultDatabaseIndex ) DefaultDatabaseIndex++;
}
/// <summary>
/// Overrides the base implementation to remove the association between the item
/// being removed and this object (i.e., sets the List property of the
/// DatabaseConnectionInfo object to null)
/// <para>The DefaultDatabaseIndex is cleared if the deleted item is the
/// existing default database connection.</para>
/// </summary>
/// <param name="index">the location in the collection of the object being removed</param>
protected override void RemoveItem( int index )
{
this[index].List = null;
if( index == DefaultDatabaseIndex ) DefaultDatabaseIndex = -1;
base.RemoveItem(index);
}
/// <summary>
/// Overrides the base implementation to associate the item being added with
/// this object (i.e., sets the List property of the item being inserted to
/// this instance) and to disassociate the item being overwritten.
/// <para>The DefaultDatabaseIndex is cleared if the new item is overwriting the
/// existing default database connection.</para>
/// </summary>
/// <param name="index">the location in the collection where the object is being set</param>
/// <param name="item">the DatabaseConnectionInfo item being inserted</param>
protected override void SetItem( int index, DatabaseConnectionInfo item )
{
if( index == DefaultDatabaseIndex ) DefaultDatabaseIndex = -1;
else
{
if( index <= DefaultDatabaseIndex ) DefaultDatabaseIndex++;
}
this[index].List = null;
item.List = this;
base.SetItem(index, item);
}
}
}