From 9bc5cd75405757a8986f140f5d29fb4badfda28b Mon Sep 17 00:00:00 2001 From: "grzegorz.russek" Date: Thu, 24 Apr 2014 19:30:35 +0000 Subject: [PATCH] --- AmalgamationTool/AmalgamationTool.csproj | 58 + AmalgamationTool/DynamORM.Amalgamation.cs | 10406 ++++++++++++++++ AmalgamationTool/Program.cs | 131 + AmalgamationTool/Properties/AssemblyInfo.cs | 36 + DynamORM.Tests/Modify/ParserTests.cs | 16 +- DynamORM.Tests/Select/DynamicAccessTests.cs | 2 +- DynamORM.Tests/Select/LegacyParserTests.cs | 6 +- DynamORM.Tests/Select/TypedAccessTests.cs | 2 +- .../Builders/IDynamicInsertQueryBuilder.cs | 7 +- .../Builders/IDynamicSelectQueryBuilder.cs | 24 +- .../Builders/IDynamicUpdateQueryBuilder.cs | 2 +- .../DynamicInsertQueryBuilder.cs | 101 +- .../DynamicSelectQueryBuilder.cs | 734 +- .../DynamicUpdateQueryBuilder.cs | 2 +- DynamORM/DynamicDatabase.cs | 42 +- DynamORM/DynamicExtensions.cs | 43 +- DynamORM/DynamicTable.cs | 24 +- DynamORM/Properties/AssemblyInfo.cs | 5 + 18 files changed, 11192 insertions(+), 449 deletions(-) create mode 100644 AmalgamationTool/AmalgamationTool.csproj create mode 100644 AmalgamationTool/DynamORM.Amalgamation.cs create mode 100644 AmalgamationTool/Program.cs create mode 100644 AmalgamationTool/Properties/AssemblyInfo.cs diff --git a/AmalgamationTool/AmalgamationTool.csproj b/AmalgamationTool/AmalgamationTool.csproj new file mode 100644 index 0000000..76fb7a7 --- /dev/null +++ b/AmalgamationTool/AmalgamationTool.csproj @@ -0,0 +1,58 @@ + + + + Debug + x86 + 8.0.30703 + 2.0 + {A64D2052-D0CD-488E-BF05-E5952615D926} + Exe + Properties + AmalgamationTool + AmalgamationTool + v4.0 + Client + 512 + + + x86 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + x86 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AmalgamationTool/DynamORM.Amalgamation.cs b/AmalgamationTool/DynamORM.Amalgamation.cs new file mode 100644 index 0000000..825a45d --- /dev/null +++ b/AmalgamationTool/DynamORM.Amalgamation.cs @@ -0,0 +1,10406 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + * + * See: http://opensource.org/licenses/bsd-license.php + * + * Supported preprocessor flags: + * * DYNAMORM_OMMIT_OLDSYNTAX - Remove dynamic table functionality + * * DYNAMORM_OMMIT_GENERICEXECUTION - Remove generic execution functionality + * * DYNAMORM_OMMIT_TRYPARSE - Remove TryParse helpers (also applies DYNAMORM_OMMIT_GENERICEXECUTION) +*/ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Data; +using System.Data.Common; +using System.Dynamic; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Text; +using DynamORM.Builders; +using DynamORM.Builders.Extensions; +using DynamORM.Builders.Implementation; +using DynamORM.Helpers; +using DynamORM.Helpers.Dynamics; +using DynamORM.Mapper; + +[module: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleClass", Justification = "This is a generated file which generates all the necessary support classes.")] +[module: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1403:FileMayOnlyContainASingleNamespace", Justification = "This is a generated file which generates all the necessary support classes.")] + +namespace DynamORM +{ + /// Small utility class to manage single columns. + public class DynamicColumn + { + #region Enums + + /// Order By Order. + public enum SortOrder + { + /// Ascending order. + Asc, + + /// Descending order. + Desc + } + + /// Dynamic query operators. + public enum CompareOperator + { + /// Equals operator (default). + Eq, + + /// Not equal operator. + Not, + + /// Like operator. + Like, + + /// Not like operator. + NotLike, + + /// In operator. + In, + + /// Less than operator. + Lt, + + /// Less or equal operator. + Lte, + + /// Greater than operator. + Gt, + + /// Greater or equal operator. + Gte, + + /// Between two values. + Between, + } + + #endregion Enums + + #region Constructors + + /// Initializes a new instance of the class. + public DynamicColumn() + { + } + + /// Initializes a new instance of the class. + /// Constructor provided for easier object creation in queries. + /// Name of column to set. + public DynamicColumn(string columnName) + : this() + { + ColumnName = columnName; + } + + /// Initializes a new instance of the class. + /// Constructor provided for easier object creation in queries. + /// Name of column to set. + /// Compare column to value(s) operator. + /// Parameter value(s). + public DynamicColumn(string columnName, CompareOperator oper, object value) + : this(columnName) + { + Operator = oper; + Value = value; + } + + #endregion Constructors + + #region Properties + + /// Gets or sets column name. + public string ColumnName { get; set; } + + /// Gets or sets column alias. + /// Select specific. + public string Alias { get; set; } + + /// Gets or sets aggregate function used on column. + /// Select specific. + public string Aggregate { get; set; } + + /// Gets or sets order direction. + public SortOrder Order { get; set; } + + /// Gets or sets value for parameters. + public object Value { get; set; } + + /// Gets or sets condition operator. + public CompareOperator Operator { get; set; } + + /// Gets or sets a value indicating whether this condition will be treated as or condition. + public bool Or { get; set; } + + /// Gets or sets a value indicating whether start new block in where statement. + public bool BeginBlock { get; set; } + + /// Gets or sets a value indicating whether end existing block in where statement. + public bool EndBlock { get; set; } + + /// Gets or sets a value indicating whether set parameters for null values. + public bool? VirtualColumn { get; set; } + + /// Gets or sets schema representation of a column. + /// Workaround to providers issues which sometimes pass wrong + /// data o schema. For example decimal has precision of 255 in SQL + /// server. + public DynamicSchemaColumn? Schema { get; set; } + + #endregion Properties + + #region Query creation helpers + + #region Operators + + private DynamicColumn SetOperatorAndValue(CompareOperator c, object v) + { + Operator = c; + Value = v; + + return this; + } + + /// Helper method setting + /// to and + /// to provided value. + /// Value of parameter to set. + /// Returns self. + public DynamicColumn Eq(object value) + { + return SetOperatorAndValue(CompareOperator.Eq, value); + } + + /// Helper method setting + /// to and + /// to provided value. + /// Value of parameter to set. + /// Returns self. + public DynamicColumn Not(object value) + { + return SetOperatorAndValue(CompareOperator.Not, value); + } + + /// Helper method setting + /// to and + /// to provided value. + /// Value of parameter to set. + /// Returns self. + public DynamicColumn Like(object value) + { + return SetOperatorAndValue(CompareOperator.Like, value); + } + + /// Helper method setting + /// to and + /// to provided value. + /// Value of parameter to set. + /// Returns self. + public DynamicColumn NotLike(object value) + { + return SetOperatorAndValue(CompareOperator.NotLike, value); + } + + /// Helper method setting + /// to and + /// to provided value. + /// Value of parameter to set. + /// Returns self. + public DynamicColumn Greater(object value) + { + return SetOperatorAndValue(CompareOperator.Gt, value); + } + + /// Helper method setting + /// to and + /// to provided value. + /// Value of parameter to set. + /// Returns self. + public DynamicColumn Less(object value) + { + return SetOperatorAndValue(CompareOperator.Lt, value); + } + + /// Helper method setting + /// to and + /// to provided value. + /// Value of parameter to set. + /// Returns self. + public DynamicColumn GreaterOrEqual(object value) + { + return SetOperatorAndValue(CompareOperator.Gte, value); + } + + /// Helper method setting + /// to and + /// to provided value. + /// Value of parameter to set. + /// Returns self. + public DynamicColumn LessOrEqual(object value) + { + return SetOperatorAndValue(CompareOperator.Lte, value); + } + + /// Helper method setting + /// to and + /// to provided values. + /// Value of from parameter to set. + /// Value of to parameter to set. + /// Returns self. + public DynamicColumn Between(object from, object to) + { + return SetOperatorAndValue(CompareOperator.Between, new[] { from, to }); + } + + /// Helper method setting + /// to and + /// to provided values. + /// Values of parameters to set. + /// Returns self. + public DynamicColumn In(IEnumerable values) + { + return SetOperatorAndValue(CompareOperator.In, values); + } + + /// Helper method setting + /// to and + /// to provided values. + /// Values of parameters to set. + /// Returns self. + public DynamicColumn In(params object[] values) + { + if (values.Length == 1 && (values[0].GetType().IsCollection() || values[0] is IEnumerable)) + return SetOperatorAndValue(CompareOperator.In, values[0]); + + return SetOperatorAndValue(CompareOperator.In, values); + } + + #endregion Operators + + #region Order + + /// Helper method setting + /// to .. + /// Returns self. + public DynamicColumn Asc() + { + Order = SortOrder.Asc; + return this; + } + + /// Helper method setting + /// to .. + /// Returns self. + public DynamicColumn Desc() + { + Order = SortOrder.Desc; + return this; + } + + #endregion Order + + #region Other + + /// Helper method setting + /// + /// to provided name. + /// Name to set. + /// Returns self. + public DynamicColumn SetName(string name) + { + ColumnName = name; + return this; + } + + /// Helper method setting + /// + /// to provided alias. + /// Alias to set. + /// Returns self. + public DynamicColumn SetAlias(string alias) + { + Alias = alias; + return this; + } + + /// Helper method setting + /// + /// to provided aggregate. + /// Aggregate to set. + /// Returns self. + public DynamicColumn SetAggregate(string aggregate) + { + Aggregate = aggregate; + return this; + } + + /// Sets the begin block flag. + /// If set to true [begin]. + /// Returns self. + public DynamicColumn SetBeginBlock(bool begin = true) + { + BeginBlock = begin; + return this; + } + + /// Sets the end block flag. + /// If set to true [end]. + /// Returns self. + public DynamicColumn SetEndBlock(bool end = true) + { + EndBlock = end; + return this; + } + + /// Sets the or flag. + /// If set to true [or]. + /// Returns self. + public DynamicColumn SetOr(bool or = true) + { + Or = or; + return this; + } + + /// Sets the virtual column. + /// Set virtual column value. + /// Returns self. + public DynamicColumn SetVirtualColumn(bool? virt) + { + VirtualColumn = virt; + return this; + } + + #endregion Other + + #endregion Query creation helpers + + #region Parsing + + /// Parse column for select query. + /// Column format consist of Column Name, Alias and + /// Aggregate function in this order separated by ':'. + /// Column string. + /// Instance of . + public static DynamicColumn ParseSelectColumn(string column) + { + // Split column description + var parts = column.Split(':'); + + if (parts.Length > 0) + { + DynamicColumn ret = new DynamicColumn() { ColumnName = parts[0] }; + + if (parts.Length > 1) + ret.Alias = parts[1]; + + if (parts.Length > 2) + ret.Aggregate = parts[2]; + + return ret; + } + + return null; + } + + /// Parse column for order by in query. + /// Column format consist of Column Name and + /// Direction in this order separated by ':'. + /// Column string. + /// Instance of . + public static DynamicColumn ParseOrderByColumn(string column) + { + // Split column description + var parts = column.Split(':'); + + if (parts.Length > 0) + { + DynamicColumn ret = new DynamicColumn() { ColumnName = parts[0] }; + + if (parts.Length > 1) + ret.Order = parts[1].ToLower() == "d" || parts[1].ToLower() == "desc" ? SortOrder.Desc : SortOrder.Asc; + + if (parts.Length > 2) + ret.Alias = parts[2]; + + return ret; + } + + return null; + } + + #endregion Parsing + + #region ToSQL + + internal string ToSQLSelectColumn(DynamicDatabase db) + { + StringBuilder sb = new StringBuilder(); + ToSQLSelectColumn(db, sb); + return sb.ToString(); + } + + internal void ToSQLSelectColumn(DynamicDatabase db, StringBuilder sb) + { + string column = ColumnName == "*" ? "*" : ColumnName; + + if (column != "*" && + (column.IndexOf(db.LeftDecorator) == -1 || column.IndexOf(db.RightDecorator) == -1) && + (column.IndexOf('(') == -1 || column.IndexOf(')') == -1)) + column = db.DecorateName(column); + + string alias = Alias; + + if (!string.IsNullOrEmpty(Aggregate)) + { + sb.AppendFormat("{0}({1})", Aggregate, column); + + alias = string.IsNullOrEmpty(alias) ? + ColumnName == "*" ? Guid.NewGuid().ToString() : ColumnName : + alias; + } + else + sb.Append(column); + + if (!string.IsNullOrEmpty(alias)) + sb.AppendFormat(" AS {0}", alias); + } + + internal string ToSQLGroupByColumn(DynamicDatabase db) + { + StringBuilder sb = new StringBuilder(); + ToSQLGroupByColumn(db, sb); + return sb.ToString(); + } + + internal void ToSQLGroupByColumn(DynamicDatabase db, StringBuilder sb) + { + sb.Append(db.DecorateName(ColumnName)); + } + + internal string ToSQLOrderByColumn(DynamicDatabase db) + { + StringBuilder sb = new StringBuilder(); + ToSQLOrderByColumn(db, sb); + return sb.ToString(); + } + + internal void ToSQLOrderByColumn(DynamicDatabase db, StringBuilder sb) + { + if (!string.IsNullOrEmpty(Alias)) + sb.Append(Alias); + else + sb.Append(db.DecorateName(ColumnName)); + + sb.AppendFormat(" {0}", Order.ToString().ToUpper()); + } + + #endregion ToSQL + } + + /// Helper class to easy manage command. + public class DynamicCommand : IDbCommand, IExtendedDisposable + { + private IDbCommand _command; + private int? _commandTimeout = null; + private DynamicConnection _con; + private DynamicDatabase _db; + ////private long _poolStamp = 0; + + /// Initializes a new instance of the class. + /// The connection. + /// The database manager. + internal DynamicCommand(DynamicConnection con, DynamicDatabase db) + { + IsDisposed = false; + _con = con; + _db = db; + + lock (_db.SyncLock) + { + if (!_db.CommandsPool.ContainsKey(_con.Connection)) + throw new InvalidOperationException("Can't create command using disposed connection."); + else + { + _command = _con.Connection.CreateCommand(); + _db.CommandsPool[_con.Connection].Add(this); + } + } + } + + /// Initializes a new instance of the class. + /// The database manager. + /// Used internally to create command without context. + internal DynamicCommand(DynamicDatabase db) + { + IsDisposed = false; + _db = db; + _command = db.Provider.CreateCommand(); + } + + /// Prepare command for execution. + /// Returns edited instance. + internal IDbCommand PrepareForExecution() + { + // TODO: Fix that + ////if (_poolStamp < _db.PoolStamp) + { + _command.CommandTimeout = _commandTimeout ?? _db.CommandTimeout ?? _command.CommandTimeout; + + if (_db.TransactionPool[_command.Connection].Count > 0) + _command.Transaction = _db.TransactionPool[_command.Connection].Peek(); + else + _command.Transaction = null; + + ////_poolStamp = _db.PoolStamp; + } + + return _db.DumpCommands ? _command.Dump(Console.Out) : _command; + } + + #region IDbCommand Members + + /// + /// Attempts to cancels the execution of an . + /// + public void Cancel() + { + _command.Cancel(); + } + + /// + /// Gets or sets the text command to run against the data source. + /// + /// The text command to execute. The default value is an empty string (""). + public string CommandText { get { return _command.CommandText; } set { _command.CommandText = value; } } + + /// + /// Gets or sets the wait time before terminating the attempt to execute a command and generating an error. + /// + /// The time (in seconds) to wait for the command to execute. The default value is 30 seconds. + /// The property value assigned is less than 0. + public int CommandTimeout { get { return _commandTimeout ?? _command.CommandTimeout; } set { _commandTimeout = value; } } + + /// Gets or sets how the property is interpreted. + public CommandType CommandType { get { return _command.CommandType; } set { _command.CommandType = value; } } + + /// Gets or sets the + /// used by this instance of the . + /// The connection to the data source. + public IDbConnection Connection + { + get { return _con; } + + set + { + _con = value as DynamicConnection; + + if (_con != null) + { + ////_poolStamp = 0; + _command.Connection = _con.Connection; + } + else if (value == null) + { + _command.Transaction = null; + _command.Connection = null; + } + else + throw new InvalidOperationException("Can't assign direct IDbConnection implementation. This property accepts only DynamORM implementation of IDbConnection."); + } + } + + /// Creates a new instance of an + /// object. + /// An object. + public IDbDataParameter CreateParameter() + { + return _command.CreateParameter(); + } + + /// Executes an SQL statement against the Connection object of a + /// data provider, and returns the number of rows affected. + /// The number of rows affected. + public int ExecuteNonQuery() + { + try + { + return PrepareForExecution().ExecuteNonQuery(); + } + catch (Exception ex) + { + throw new DynamicQueryException(ex, this); + } + } + + /// Executes the + /// against the , + /// and builds an using one + /// of the values. + /// One of the + /// values. + /// An object. + public IDataReader ExecuteReader(CommandBehavior behavior) + { + try + { + return PrepareForExecution().ExecuteReader(behavior); + } + catch (Exception ex) + { + throw new DynamicQueryException(ex, this); + } + } + + /// Executes the + /// against the and + /// builds an . + /// An object. + public IDataReader ExecuteReader() + { + try + { + return PrepareForExecution().ExecuteReader(); + } + catch (Exception ex) + { + throw new DynamicQueryException(ex, this); + } + } + + /// Executes the query, and returns the first column of the + /// first row in the result set returned by the query. Extra columns or + /// rows are ignored. + /// The first column of the first row in the result set. + public object ExecuteScalar() + { + try + { + return PrepareForExecution().ExecuteScalar(); + } + catch (Exception ex) + { + throw new DynamicQueryException(ex, this); + } + } + + /// Gets the . + public IDataParameterCollection Parameters + { + get { return _command.Parameters; } + } + + /// Creates a prepared (or compiled) version of the command on the data source. + public void Prepare() + { + try + { + _command.Prepare(); + } + catch (Exception ex) + { + throw new DynamicQueryException("Error preparing command.", ex, this); + } + } + + /// Gets or sets the transaction within which the Command + /// object of a data provider executes. + /// It's does nothing, transaction is peeked from transaction + /// pool of a connection. This is only a dummy. + public IDbTransaction Transaction { get { return null; } set { } } + + /// Gets or sets how command results are applied to the + /// when used by the + /// method of a . + /// One of the values. The default is + /// Both unless the command is automatically generated. Then the default is None. + /// The value entered was not one of the + /// values. + public UpdateRowSource UpdatedRowSource { get { return _command.UpdatedRowSource; } set { _command.UpdatedRowSource = value; } } + + #endregion IDbCommand Members + + #region IExtendedDisposable Members + + /// Performs application-defined tasks associated with + /// freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + lock (_db.SyncLock) + { + if (_con != null) + { + var pool = _db.CommandsPool.TryGetValue(_con.Connection); + + if (pool != null && pool.Contains(this)) + pool.Remove(this); + } + + IsDisposed = true; + + _command.Dispose(); + } + } + + /// Gets a value indicating whether this instance is disposed. + public bool IsDisposed { get; private set; } + + #endregion IExtendedDisposable Members + } + + /// Connection wrapper. + /// This class is only connection holder, connection is managed by + /// instance. + public class DynamicConnection : IDbConnection, IExtendedDisposable + { + private DynamicDatabase _db; + private bool _singleTransaction; + + /// Gets underlying connection. + internal IDbConnection Connection { get; private set; } + + /// Initializes a new instance of the class. + /// Database connection manager. + /// Active connection. + /// Are we using single transaction mode? I so... act correctly. + internal DynamicConnection(DynamicDatabase db, IDbConnection con, bool singleTransaction) + { + IsDisposed = false; + _db = db; + Connection = con; + _singleTransaction = singleTransaction; + } + + /// Begins a database transaction. + /// One of the values. + /// This action is invoked when transaction is disposed. + /// Returns representation. + internal DynamicTransaction BeginTransaction(IsolationLevel? il, Action disposed) + { + return new DynamicTransaction(_db, this, _singleTransaction, il, disposed); + } + + #region IDbConnection Members + + /// Creates and returns a Command object associated with the connection. + /// A Command object associated with the connection. + public IDbCommand CreateCommand() + { + return new DynamicCommand(this, _db); + } + + /// Begins a database transaction. + /// Returns representation. + public IDbTransaction BeginTransaction() + { + return BeginTransaction(null, null); + } + + /// Begins a database transaction with the specified + /// value. + /// One of the values. + /// Returns representation. + public IDbTransaction BeginTransaction(IsolationLevel il) + { + return BeginTransaction(il, null); + } + + /// Changes the current database for an open Connection object. + /// The name of the database to use in place of the current database. + /// This operation is not supported in DynamORM. and will throw . + /// Thrown always. + public void ChangeDatabase(string databaseName) + { + throw new NotSupportedException("This operation is not supported in DynamORM."); + } + + /// Opens a database connection with the settings specified by + /// the ConnectionString property of the provider-specific + /// Connection object. + /// Does nothing. handles + /// opening connections. + public void Open() + { + } + + /// Closes the connection to the database. + /// Does nothing. handles + /// closing connections. Only way to close it is to dispose connection. + /// It will close if this is multi connection configuration, otherwise + /// it will stay open until is not + /// disposed. + public void Close() + { + } + + /// Gets or sets the string used to open a database. + /// Changing connection string operation is not supported in DynamORM. + /// and will throw . + /// Thrown always when set is attempted. + public string ConnectionString + { + get { return Connection.ConnectionString; } + set { throw new NotSupportedException("This operation is not supported in DynamORM."); } + } + + /// Gets the time to wait while trying to establish a connection + /// before terminating the attempt and generating an error. + public int ConnectionTimeout + { + get { return Connection.ConnectionTimeout; } + } + + /// Gets the name of the current database or the database + /// to be used after a connection is opened. + public string Database + { + get { throw new NotImplementedException(); } + } + + /// Gets the current state of the connection. + public ConnectionState State + { + get { return Connection.State; } + } + + #endregion IDbConnection Members + + #region IExtendedDisposable Members + + /// Performs application-defined tasks associated with freeing, + /// releasing, or resetting unmanaged resources. + public void Dispose() + { + _db.Close(Connection); + IsDisposed = true; + } + + /// Gets a value indicating whether this instance is disposed. + public bool IsDisposed { get; private set; } + + #endregion IExtendedDisposable Members + } + + /// Dynamic database is a class responsible for managing database. + public class DynamicDatabase : IExtendedDisposable + { + #region Internal fields and properties + + private DbProviderFactory _provider; + private string _connectionString; + private bool _singleConnection; + private bool _singleTransaction; + private string _leftDecorator = "\""; + private string _rightDecorator = "\""; + private bool _leftDecoratorIsInInvalidMembersChars = true; + private bool _rightDecoratorIsInInvalidMembersChars = true; + private string _parameterFormat = "@{0}"; + private int? _commandTimeout = null; + private long _poolStamp = 0; + + private DynamicConnection _tempConn = null; + + /// Provides lock object for this database instance. + internal readonly object SyncLock = new object(); + + /// Gets or sets timestamp of last transaction pool or configuration change. + /// This property is used to allow commands to determine if + /// they need to update transaction object or not. + internal long PoolStamp + { + get + { + long r = 0; + + lock (SyncLock) + r = _poolStamp; + + return r; + } + + set + { + lock (SyncLock) + _poolStamp = value; + } + } + + /// Gets pool of connections and transactions. + internal Dictionary> TransactionPool { get; private set; } + + /// Gets pool of connections and commands. + /// Pool should contain dynamic commands instead of native ones. + internal Dictionary> CommandsPool { get; private set; } + + /// Gets schema columns cache. + internal Dictionary> Schema { get; private set; } + +#if !DYNAMORM_OMMIT_OLDSYNTAX + + /// Gets tables cache for this database instance. + internal Dictionary TablesCache { get; private set; } + +#endif + + #endregion Internal fields and properties + + #region Properties and Constructors + + /// Gets database options. + public DynamicDatabaseOptions Options { get; private set; } + + /// Gets or sets command timeout. + public int? CommandTimeout { get { return _commandTimeout; } set { _commandTimeout = value; _poolStamp = DateTime.Now.Ticks; } } + + /// Gets the database provider. + public DbProviderFactory Provider { get { return _provider; } } + + /// Gets or sets a value indicating whether + /// dump commands to console or not. + public bool DumpCommands { get; set; } + + /// Initializes a new instance of the class. + /// Database provider by name. + /// Connection string to provided database. + /// Connection options. + public DynamicDatabase(string provider, string connectionString, DynamicDatabaseOptions options) + : this(DbProviderFactories.GetFactory(provider), connectionString, options) + { + } + + /// Initializes a new instance of the class. + /// Database provider. + /// Connection string to provided database. + /// Connection options. + public DynamicDatabase(DbProviderFactory provider, string connectionString, DynamicDatabaseOptions options) + { + IsDisposed = false; + _provider = provider; + + InitCommon(connectionString, options); + } + + /// Initializes a new instance of the class. + /// Active database connection. + /// Connection options. required. + public DynamicDatabase(IDbConnection connection, DynamicDatabaseOptions options) + { + IsDisposed = false; + InitCommon(connection.ConnectionString, options); + TransactionPool.Add(connection, new Stack()); + + if (!_singleConnection) + throw new InvalidOperationException("This constructor accepts only connections with DynamicDatabaseOptions.SingleConnection option."); + } + + private void InitCommon(string connectionString, DynamicDatabaseOptions options) + { + _connectionString = connectionString; + Options = options; + + _singleConnection = (options & DynamicDatabaseOptions.SingleConnection) == DynamicDatabaseOptions.SingleConnection; + _singleTransaction = (options & DynamicDatabaseOptions.SingleTransaction) == DynamicDatabaseOptions.SingleTransaction; + DumpCommands = (options & DynamicDatabaseOptions.DumpCommands) == DynamicDatabaseOptions.DumpCommands; + + TransactionPool = new Dictionary>(); + CommandsPool = new Dictionary>(); + Schema = new Dictionary>(); +#if !DYNAMORM_OMMIT_OLDSYNTAX + TablesCache = new Dictionary(); +#endif + } + + #endregion Properties and Constructors + + #region Table + +#if !DYNAMORM_OMMIT_OLDSYNTAX + + /// Gets dynamic table which is a simple ORM using dynamic objects. + /// The action with instance of as parameter. + /// Table name. + /// Override keys in schema. + /// Owner of the table. + public void Table(Action action, string table = "", string[] keys = null, string owner = "") + { + using (dynamic t = Table(table, keys, owner)) + action(t); + } + + /// Gets dynamic table which is a simple ORM using dynamic objects. + /// Type used to determine table name. + /// The action with instance of as parameter. + /// Override keys in schema. + public void Table(Action action, string[] keys = null) + { + using (dynamic t = Table(keys)) + action(t); + } + + /// Gets dynamic table which is a simple ORM using dynamic objects. + /// Table name. + /// Override keys in schema. + /// Owner of the table. + /// Instance of . + public dynamic Table(string table = "", string[] keys = null, string owner = "") + { + string key = string.Concat( + table == null ? string.Empty : table, + keys == null ? string.Empty : string.Join("_|_", keys)); + + DynamicTable dt = null; + lock (SyncLock) + dt = TablesCache.TryGetValue(key) ?? + TablesCache.AddAndPassValue(key, + new DynamicTable(this, table, owner, keys)); + + return dt; + } + + /// Gets dynamic table which is a simple ORM using dynamic objects. + /// Type used to determine table name. + /// Override keys in schema. + /// Instance of . + public dynamic Table(string[] keys = null) + { + Type table = typeof(T); + string key = string.Concat( + table.FullName, + keys == null ? string.Empty : string.Join("_|_", keys)); + + DynamicTable dt = null; + lock (SyncLock) + dt = TablesCache.TryGetValue(key) ?? + TablesCache.AddAndPassValue(key, + new DynamicTable(this, table, keys)); + + return dt; + } + + /// Removes cached table. + /// Disposed dynamic table. + internal void RemoveFromCache(DynamicTable dynamicTable) + { + foreach (var item in TablesCache.Where(kvp => kvp.Value == dynamicTable).ToList()) + TablesCache.Remove(item.Key); + } + +#endif + + #endregion Table + + #region From/Insert/Update/Delete + + /// + /// Adds to the FROM clause the contents obtained by parsing the dynamic lambda expressions given. The supported + /// formats are: + /// - Resolve to a string: x => "owner.Table AS Alias", where the alias part is optional. + /// - Resolve to an expression: x => x.owner.Table.As( x.Alias ), where the alias part is optional. + /// - Generic expression: x => x( expression ).As( x.Alias ), where the alias part is mandatory. In this + /// case the alias is not annotated. + /// + /// The specification. + /// This instance to permit chaining. + public virtual IDynamicSelectQueryBuilder From(Func fn) + { + return new DynamicSelectQueryBuilder(this).From(fn); + } + + /// Adds to the FROM clause using . + /// Type which can be represented in database. + /// This instance to permit chaining. + public virtual IDynamicSelectQueryBuilder From() + { + return new DynamicSelectQueryBuilder(this).From(x => x(typeof(T))); + } + + /// Adds to the FROM clause using . + /// Type which can be represented in database. + /// This instance to permit chaining. + public virtual IDynamicSelectQueryBuilder From(Type t) + { + return new DynamicSelectQueryBuilder(this).From(x => x(t)); + } + + /// + /// Adds to the INSERT INTO clause the contents obtained by parsing the dynamic lambda expressions given. The supported + /// formats are: + /// - Resolve to a string: x => "owner.Table". + /// - Resolve to a type: x => typeof(SomeClass). + /// - Resolve to an expression: x => x.owner.Table. + /// - Generic expression: x => x( expression ). Expression can + /// be or . + /// + /// The specification. + /// This instance to permit chaining. + public virtual IDynamicInsertQueryBuilder Insert(Func func) + { + return new DynamicInsertQueryBuilder(this).Table(func); + } + + /// Adds to the INSERT INTO clause using . + /// Type which can be represented in database. + /// This instance to permit chaining. + public virtual IDynamicInsertQueryBuilder Insert() + { + return new DynamicInsertQueryBuilder(this).Table(typeof(T)); + } + + /// Adds to the INSERT INTO clause using . + /// Type which can be represented in database. + /// This instance to permit chaining. + public virtual IDynamicInsertQueryBuilder Insert(Type t) + { + return new DynamicInsertQueryBuilder(this).Table(t); + } + + /// Bulk insert objects into database. + /// Type of objects to insert. + /// Enumerable containing instances of objects to insert. + /// Number of inserted rows. + public virtual int Insert(IEnumerable e) where T : class + { + int affected = 0; + var mapper = DynamicMapperCache.GetMapper(typeof(T)); + + if (mapper != null) + { + using (var con = Open()) + using (var tra = con.BeginTransaction()) + using (var cmd = con.CreateCommand()) + { + try + { + var parameters = new Dictionary(); + + if (!string.IsNullOrEmpty(mapper.InsertCommandText)) + { + cmd.CommandText = mapper.InsertCommandText; + + foreach (var col in mapper.ColumnsMap.Values + .Where(di => !di.Ignore && di.InsertCommandParameter != null) + .OrderBy(di => di.InsertCommandParameter.Ordinal)) + { + var para = cmd.CreateParameter(); + para.ParameterName = col.InsertCommandParameter.Name; + para.DbType = col.InsertCommandParameter.Type; + cmd.Parameters.Add(para); + + parameters[para] = col; + } + } + else + { + DynamicPropertyInvoker currentprop = null; + var temp = new Dictionary(); + int ord = 0; + + var ib = Insert() + .SetVirtualMode(true) + .CreateTemporaryParameterAction(p => temp[p.Name] = currentprop) + .CreateParameterAction((p, cp) => + { + parameters[cp] = temp[p.Name]; + parameters[cp].InsertCommandParameter = new DynamicPropertyInvoker.ParameterSpec + { + Name = cp.ParameterName, + Type = cp.DbType, + Ordinal = ord++, + }; + }); + + foreach (var prop in mapper.PropertyMap) + if (!mapper.Ignored.Contains(prop.Key)) + { + var col = mapper.PropertyMap.TryGetValue(prop.Key) ?? prop.Key; + currentprop = mapper.ColumnsMap.TryGetValue(col.ToLower()); + + if (currentprop.Ignore) + continue; + + if (currentprop.Get != null) + ib.Insert(col, null); + } + + ib.FillCommand(cmd); + + // Cache command + mapper.InsertCommandText = cmd.CommandText; + } + + foreach (var o in e) + { + foreach (var m in parameters) + m.Key.Value = m.Value.Get(o); + + affected += cmd.ExecuteNonQuery(); + } + + tra.Commit(); + } + catch (Exception ex) + { + if (tra != null) + tra.Rollback(); + + affected = 0; + + var problematicCommand = new StringBuilder(); + cmd.Dump(problematicCommand); + + throw new InvalidOperationException(problematicCommand.ToString(), ex); + } + } + } + + return affected; + } + + /// + /// Adds to the UPDATE clause the contents obtained by parsing the dynamic lambda expressions given. The supported + /// formats are: + /// - Resolve to a string: x => "owner.Table". + /// - Resolve to a type: x => typeof(SomeClass). + /// - Resolve to an expression: x => x.owner.Table. + /// - Generic expression: x => x( expression ). Expression can + /// be or . + /// + /// The specification. + /// This instance to permit chaining. + public virtual IDynamicUpdateQueryBuilder Update(Func func) + { + return new DynamicUpdateQueryBuilder(this).Table(func); + } + + /// Adds to the UPDATE clause using . + /// Type which can be represented in database. + /// This instance to permit chaining. + public virtual IDynamicUpdateQueryBuilder Update() + { + return new DynamicUpdateQueryBuilder(this).Table(typeof(T)); + } + + /// Adds to the UPDATE clause using . + /// Type which can be represented in database. + /// This instance to permit chaining. + public virtual IDynamicUpdateQueryBuilder Update(Type t) + { + return new DynamicUpdateQueryBuilder(this).Table(t); + } + + /// Bulk update objects in database. + /// Type of objects to update. + /// Enumerable containing instances of objects to update. + /// Number of updated rows. + public virtual int Update(IEnumerable e) where T : class + { + int affected = 0; + var mapper = DynamicMapperCache.GetMapper(typeof(T)); + + if (mapper != null) + { + using (var con = Open()) + using (var tra = con.BeginTransaction()) + using (var cmd = con.CreateCommand()) + { + try + { + var parameters = new Dictionary(); + + if (!string.IsNullOrEmpty(mapper.UpdateCommandText)) + { + cmd.CommandText = mapper.UpdateCommandText; + + foreach (var col in mapper.ColumnsMap.Values + .Where(di => !di.Ignore && di.UpdateCommandParameter != null) + .OrderBy(di => di.UpdateCommandParameter.Ordinal)) + { + var para = cmd.CreateParameter(); + para.ParameterName = col.UpdateCommandParameter.Name; + para.DbType = col.UpdateCommandParameter.Type; + cmd.Parameters.Add(para); + + parameters[para] = col; + } + } + else + { + DynamicPropertyInvoker currentprop = null; + var temp = new Dictionary(); + int ord = 0; + + var ib = Update() + .SetVirtualMode(true) + .CreateTemporaryParameterAction(p => temp[p.Name] = currentprop) + .CreateParameterAction((p, cp) => + { + parameters[cp] = temp[p.Name]; + parameters[cp].UpdateCommandParameter = new DynamicPropertyInvoker.ParameterSpec + { + Name = cp.ParameterName, + Type = cp.DbType, + Ordinal = ord++, + }; + }); + + foreach (var prop in mapper.PropertyMap) + if (!mapper.Ignored.Contains(prop.Key)) + { + var col = mapper.PropertyMap.TryGetValue(prop.Key) ?? prop.Key; + currentprop = mapper.ColumnsMap.TryGetValue(col.ToLower()); + + if (currentprop.Ignore) + continue; + + if (currentprop.Get != null) + { + if (currentprop.Column != null && currentprop.Column.IsKey) + ib.Where(col, null); + else + ib.Values(col, null); + } + } + + ib.FillCommand(cmd); + + // Cache command + mapper.UpdateCommandText = cmd.CommandText; + } + + foreach (var o in e) + { + foreach (var m in parameters) + m.Key.Value = m.Value.Get(o); + + affected += cmd.ExecuteNonQuery(); + } + + tra.Commit(); + } + catch (Exception ex) + { + if (tra != null) + tra.Rollback(); + + affected = 0; + + var problematicCommand = new StringBuilder(); + cmd.Dump(problematicCommand); + + throw new InvalidOperationException(problematicCommand.ToString(), ex); + } + } + } + + return affected; + } + + /// Bulk update or insert objects into database. + /// Type of objects to update or insert. + /// Enumerable containing instances of objects to update or insert. + /// Number of updated or inserted rows. + public virtual int UpdateOrInsert(IEnumerable e) where T : class + { + int affected = 0; + var mapper = DynamicMapperCache.GetMapper(typeof(T)); + + if (mapper != null) + { + using (var con = Open()) + using (var tra = con.BeginTransaction()) + using (var cmdUp = con.CreateCommand()) + using (var cmdIn = con.CreateCommand()) + { + try + { + #region Update + + var parametersUp = new Dictionary(); + + if (!string.IsNullOrEmpty(mapper.UpdateCommandText)) + { + cmdUp.CommandText = mapper.UpdateCommandText; + + foreach (var col in mapper.ColumnsMap.Values + .Where(di => !di.Ignore && di.UpdateCommandParameter != null) + .OrderBy(di => di.UpdateCommandParameter.Ordinal)) + { + var para = cmdUp.CreateParameter(); + para.ParameterName = col.UpdateCommandParameter.Name; + para.DbType = col.UpdateCommandParameter.Type; + cmdUp.Parameters.Add(para); + + parametersUp[para] = col; + } + } + else + { + DynamicPropertyInvoker currentprop = null; + var temp = new Dictionary(); + int ord = 0; + + var ib = Update() + .SetVirtualMode(true) + .CreateTemporaryParameterAction(p => temp[p.Name] = currentprop) + .CreateParameterAction((p, cp) => + { + parametersUp[cp] = temp[p.Name]; + parametersUp[cp].UpdateCommandParameter = new DynamicPropertyInvoker.ParameterSpec + { + Name = cp.ParameterName, + Type = cp.DbType, + Ordinal = ord++, + }; + }); + + foreach (var prop in mapper.PropertyMap) + if (!mapper.Ignored.Contains(prop.Key)) + { + var col = mapper.PropertyMap.TryGetValue(prop.Key) ?? prop.Key; + currentprop = mapper.ColumnsMap.TryGetValue(col.ToLower()); + + if (currentprop.Ignore) + continue; + + if (currentprop.Get != null) + { + if (currentprop.Column != null && currentprop.Column.IsKey) + ib.Where(col, null); + else + ib.Values(col, null); + } + } + + ib.FillCommand(cmdUp); + + // Cache command + mapper.UpdateCommandText = cmdUp.CommandText; + } + + #endregion Update + + #region Insert + + var parametersIn = new Dictionary(); + + if (!string.IsNullOrEmpty(mapper.InsertCommandText)) + { + cmdIn.CommandText = mapper.InsertCommandText; + + foreach (var col in mapper.ColumnsMap.Values + .Where(di => !di.Ignore && di.InsertCommandParameter != null) + .OrderBy(di => di.InsertCommandParameter.Ordinal)) + { + var para = cmdIn.CreateParameter(); + para.ParameterName = col.InsertCommandParameter.Name; + para.DbType = col.InsertCommandParameter.Type; + cmdIn.Parameters.Add(para); + + parametersIn[para] = col; + } + } + else + { + DynamicPropertyInvoker currentprop = null; + var temp = new Dictionary(); + int ord = 0; + + var ib = Insert() + .SetVirtualMode(true) + .CreateTemporaryParameterAction(p => temp[p.Name] = currentprop) + .CreateParameterAction((p, cp) => + { + parametersIn[cp] = temp[p.Name]; + parametersIn[cp].InsertCommandParameter = new DynamicPropertyInvoker.ParameterSpec + { + Name = cp.ParameterName, + Type = cp.DbType, + Ordinal = ord++, + }; + }); + + foreach (var prop in mapper.PropertyMap) + if (!mapper.Ignored.Contains(prop.Key)) + { + var col = mapper.PropertyMap.TryGetValue(prop.Key) ?? prop.Key; + currentprop = mapper.ColumnsMap.TryGetValue(col.ToLower()); + + if (currentprop.Ignore) + continue; + + if (currentprop.Get != null) + ib.Insert(col, null); + } + + ib.FillCommand(cmdIn); + + // Cache command + mapper.InsertCommandText = cmdIn.CommandText; + } + + #endregion Insert + + foreach (var o in e) + { + foreach (var m in parametersUp) + m.Key.Value = m.Value.Get(o); + + int a = cmdUp.ExecuteNonQuery(); + if (a == 0) + { + foreach (var m in parametersIn) + m.Key.Value = m.Value.Get(o); + + a = cmdIn.ExecuteNonQuery(); + } + + affected += a; + } + + tra.Commit(); + } + catch (Exception ex) + { + if (tra != null) + tra.Rollback(); + + affected = 0; + + var problematicCommand = new StringBuilder(); + cmdUp.Dump(problematicCommand); + + throw new InvalidOperationException(problematicCommand.ToString(), ex); + } + } + } + + return affected; + } + + /// + /// Adds to the DELETE FROM clause the contents obtained by parsing the dynamic lambda expressions given. The supported + /// formats are: + /// - Resolve to a string: x => "owner.Table". + /// - Resolve to a type: x => typeof(SomeClass). + /// - Resolve to an expression: x => x.owner.Table. + /// - Generic expression: x => x( expression ). Expression can + /// be or . + /// + /// The specification. + /// This instance to permit chaining. + public virtual IDynamicDeleteQueryBuilder Delete(Func func) + { + return new DynamicDeleteQueryBuilder(this).Table(func); + } + + /// Adds to the DELETE FROM clause using . + /// Type which can be represented in database. + /// This instance to permit chaining. + public virtual IDynamicDeleteQueryBuilder Delete() + { + return new DynamicDeleteQueryBuilder(this).Table(typeof(T)); + } + + /// Bulk delete objects in database. + /// Type of objects to delete. + /// Enumerable containing instances of objects to delete. + /// Number of deleted rows. + public virtual int Delete(IEnumerable e) where T : class + { + int affected = 0; + var mapper = DynamicMapperCache.GetMapper(typeof(T)); + + if (mapper != null) + { + using (var con = Open()) + using (var tra = con.BeginTransaction()) + using (var cmd = con.CreateCommand()) + { + try + { + var parameters = new Dictionary(); + + if (!string.IsNullOrEmpty(mapper.DeleteCommandText)) + { + cmd.CommandText = mapper.DeleteCommandText; + + foreach (var col in mapper.ColumnsMap.Values + .Where(di => !di.Ignore && di.DeleteCommandParameter != null) + .OrderBy(di => di.DeleteCommandParameter.Ordinal)) + { + var para = cmd.CreateParameter(); + para.ParameterName = col.DeleteCommandParameter.Name; + para.DbType = col.DeleteCommandParameter.Type; + cmd.Parameters.Add(para); + + parameters[para] = col; + } + } + else + { + DynamicPropertyInvoker currentprop = null; + var temp = new Dictionary(); + int ord = 0; + + var ib = Delete() + .SetVirtualMode(true) + .CreateTemporaryParameterAction(p => temp[p.Name] = currentprop) + .CreateParameterAction((p, cp) => + { + parameters[cp] = temp[p.Name]; + parameters[cp].DeleteCommandParameter = new DynamicPropertyInvoker.ParameterSpec + { + Name = cp.ParameterName, + Type = cp.DbType, + Ordinal = ord++, + }; + }); + + foreach (var prop in mapper.PropertyMap) + if (!mapper.Ignored.Contains(prop.Key)) + { + var col = mapper.PropertyMap.TryGetValue(prop.Key) ?? prop.Key; + currentprop = mapper.ColumnsMap.TryGetValue(col.ToLower()); + + if (currentprop.Ignore) + continue; + + if (currentprop.Get != null) + { + if (currentprop.Column != null && currentprop.Column.IsKey) + ib.Where(col, null); + } + } + + ib.FillCommand(cmd); + + // Cache command + mapper.DeleteCommandText = cmd.CommandText; + } + + foreach (var o in e) + { + foreach (var m in parameters) + m.Key.Value = m.Value.Get(o); + + affected += cmd.ExecuteNonQuery(); + } + + tra.Commit(); + } + catch (Exception ex) + { + if (tra != null) + tra.Rollback(); + + affected = 0; + + var problematicCommand = new StringBuilder(); + cmd.Dump(problematicCommand); + + throw new InvalidOperationException(problematicCommand.ToString(), ex); + } + } + } + + return affected; + } + + #endregion From/Insert/Update/Delete + + #region Schema + + /// Builds table cache if necessary and returns it. + /// Name of table for which build schema. + /// Owner of table for which build schema. + /// Table schema. + public Dictionary GetSchema(string table, string owner = null) + { + Dictionary schema = null; + + lock (SyncLock) + schema = Schema.TryGetValue(table.ToLower()) ?? + BuildAndCacheSchema(table, null, owner); + + return schema; + } + + /// Builds table cache if necessary and returns it. + /// Type of table for which build schema. + /// Table schema or null if type was anonymous. + public Dictionary GetSchema() + { + if (typeof(T).IsAnonymous()) + return null; + + Dictionary schema = null; + + lock (SyncLock) + schema = Schema.TryGetValue(typeof(T).GetType().FullName) ?? + BuildAndCacheSchema(null, DynamicMapperCache.GetMapper()); + + return schema; + } + + /// Builds table cache if necessary and returns it. + /// Type of table for which build schema. + /// Table schema or null if type was anonymous. + public Dictionary GetSchema(Type table) + { + if (table == null || table.IsAnonymous() || table.IsValueType) + return null; + + Dictionary schema = null; + + lock (SyncLock) + schema = Schema.TryGetValue(table.FullName) ?? + BuildAndCacheSchema(null, DynamicMapperCache.GetMapper(table)); + + return schema; + } + + /// Get schema describing objects from reader. + /// Table from which extract column info. + /// Owner of table from which extract column info. + /// List of objects . + /// If your database doesn't get those values in upper case (like most of the databases) you should override this method. + protected virtual IEnumerable ReadSchema(string table, string owner) + { + using (var con = Open()) + using (var cmd = con.CreateCommand()) + { + using (var rdr = cmd + .SetCommand(string.Format("SELECT * FROM {0}{1} WHERE 1 = 0", + !string.IsNullOrEmpty(owner) ? string.Format("{0}.", DecorateName(owner)) : string.Empty, + DecorateName(table))) + .ExecuteReader(CommandBehavior.SchemaOnly | CommandBehavior.KeyInfo)) + foreach (DataRow col in rdr.GetSchemaTable().Rows) + { + var c = col.RowToDynamicUpper(); + + yield return new DynamicSchemaColumn + { + Name = c.COLUMNNAME, + Type = DynamicExtensions.TypeMap.TryGetNullable((Type)c.DATATYPE) ?? DbType.String, + IsKey = c.ISKEY ?? false, + IsUnique = c.ISUNIQUE ?? false, + Size = (int)(c.COLUMNSIZE ?? 0), + Precision = (byte)(c.NUMERICPRECISION ?? 0), + Scale = (byte)(c.NUMERICSCALE ?? 0) + }; + } + } + } + + private Dictionary BuildAndCacheSchema(string tableName, DynamicTypeMap mapper, string owner = null) + { + Dictionary schema = null; + + if (mapper != null) + tableName = mapper.Table == null || string.IsNullOrEmpty(mapper.Table.Name) ? + mapper.Type.Name : mapper.Table.Name; + + bool databaseSchemaSupport = !string.IsNullOrEmpty(tableName) && + (Options & DynamicDatabaseOptions.SupportSchema) == DynamicDatabaseOptions.SupportSchema; + bool mapperSchema = mapper != null && mapper.Table != null && (mapper.Table.Override || !databaseSchemaSupport); + + #region Database schema + + if (databaseSchemaSupport && !Schema.ContainsKey(tableName.ToLower())) + { + schema = ReadSchema(tableName, owner) + .ToDictionary(k => k.Name.ToLower(), k => k); + + Schema[tableName.ToLower()] = schema; + } + + #endregion Database schema + + #region Type schema + + if (mapperSchema && !Schema.ContainsKey(mapper.Type.FullName)) + { + // TODO: Ged rid of this monster below... + if (databaseSchemaSupport) + { + #region Merge with db schema + + schema = mapper.ColumnsMap.ToDictionary(k => k.Key, (v) => + { + DynamicSchemaColumn? col = Schema[tableName.ToLower()].TryGetNullable(v.Key); + + return new DynamicSchemaColumn + { + Name = DynamicExtensions.Coalesce( + v.Value.Column == null || string.IsNullOrEmpty(v.Value.Column.Name) ? null : v.Value.Column.Name, + col.HasValue && !string.IsNullOrEmpty(col.Value.Name) ? col.Value.Name : null, + v.Value.Name), + IsKey = DynamicExtensions.CoalesceNullable( + v.Value.Column != null ? v.Value.Column.IsKey : false, + col.HasValue ? col.Value.IsKey : false).Value, + Type = DynamicExtensions.CoalesceNullable( + v.Value.Column != null ? v.Value.Column.Type : null, + col.HasValue ? col.Value.Type : DbType.String).Value, + IsUnique = DynamicExtensions.CoalesceNullable( + v.Value.Column != null ? v.Value.Column.IsUnique : null, + col.HasValue ? col.Value.IsUnique : false).Value, + Size = DynamicExtensions.CoalesceNullable( + v.Value.Column != null ? v.Value.Column.Size : null, + col.HasValue ? col.Value.Size : 0).Value, + Precision = DynamicExtensions.CoalesceNullable( + v.Value.Column != null ? v.Value.Column.Precision : null, + col.HasValue ? col.Value.Precision : (byte)0).Value, + Scale = DynamicExtensions.CoalesceNullable( + v.Value.Column != null ? v.Value.Column.Scale : null, + col.HasValue ? col.Value.Scale : (byte)0).Value, + }; + }); + + #endregion Merge with db schema + } + else + { + #region MapEnumerable based only on type + + schema = mapper.ColumnsMap.ToDictionary(k => k.Key, + v => new DynamicSchemaColumn + { + Name = DynamicExtensions.Coalesce(v.Value.Column == null || string.IsNullOrEmpty(v.Value.Column.Name) ? null : v.Value.Column.Name, v.Value.Name), + IsKey = DynamicExtensions.CoalesceNullable(v.Value.Column != null ? v.Value.Column.IsKey : false, false).Value, + Type = DynamicExtensions.CoalesceNullable(v.Value.Column != null ? v.Value.Column.Type : null, DbType.String).Value, + IsUnique = DynamicExtensions.CoalesceNullable(v.Value.Column != null ? v.Value.Column.IsUnique : null, false).Value, + Size = DynamicExtensions.CoalesceNullable(v.Value.Column != null ? v.Value.Column.Size : null, 0).Value, + Precision = DynamicExtensions.CoalesceNullable(v.Value.Column != null ? v.Value.Column.Precision : null, 0).Value, + Scale = DynamicExtensions.CoalesceNullable(v.Value.Column != null ? v.Value.Column.Scale : null, 0).Value, + }); + + #endregion MapEnumerable based only on type + } + } + + if (mapper != null && schema != null) + Schema[mapper.Type.FullName] = schema; + + #endregion Type schema + + return schema; + } + + #endregion Schema + + #region Decorators + + /// Gets or sets left side decorator for database objects. + public string LeftDecorator + { + get { return _leftDecorator; } + set + { + _leftDecorator = value; + _leftDecoratorIsInInvalidMembersChars = + _leftDecorator.Length == 1 && StringExtensions.InvalidMemberChars.Contains(_leftDecorator[0]); + } + } + + /// Gets or sets right side decorator for database objects. + public string RightDecorator + { + get { return _rightDecorator; } + set + { + _rightDecorator = value; + _rightDecoratorIsInInvalidMembersChars = + _rightDecorator.Length == 1 && StringExtensions.InvalidMemberChars.Contains(_rightDecorator[0]); + } + } + + /// Gets or sets parameter name format. + public string ParameterFormat { get { return _parameterFormat; } set { _parameterFormat = value; } } + + /// Decorate string representing name of database object. + /// Name of database object. + /// Decorated name of database object. + public string DecorateName(string name) + { + return String.Concat(_leftDecorator, name, _rightDecorator); + } + + /// Strip string representing name of database object from decorators. + /// Decorated name of database object. + /// Not decorated name of database object. + public string StripName(string name) + { + string res = name.Trim(StringExtensions.InvalidMemberChars); + + if (!_leftDecoratorIsInInvalidMembersChars && res.StartsWith(_leftDecorator)) + res = res.Substring(_leftDecorator.Length); + + if (!_rightDecoratorIsInInvalidMembersChars && res.EndsWith(_rightDecorator)) + res = res.Substring(0, res.Length - _rightDecorator.Length); + + return res; + } + + /// Decorate string representing name of database object. + /// String builder to which add decorated name. + /// Name of database object. + public void DecorateName(StringBuilder sb, string name) + { + sb.Append(_leftDecorator); + sb.Append(name); + sb.Append(_rightDecorator); + } + + /// Get database parameter name. + /// Friendly parameter name or number. + /// Formatted parameter name. + public string GetParameterName(object parameter) + { + return String.Format(_parameterFormat, parameter).Replace(" ", "_"); + } + + /// Get database parameter name. + /// String builder to which add parameter name. + /// Friendly parameter name or number. + public void GetParameterName(StringBuilder sb, object parameter) + { + sb.AppendFormat(_parameterFormat, parameter.ToString().Replace(" ", "_")); + } + + #endregion Decorators + + #region Connection + + /// Open managed connection. + /// Opened connection. + public IDbConnection Open() + { + IDbConnection conn = null; + DynamicConnection ret = null; + bool opened = false; + + lock (SyncLock) + { + if (_tempConn == null) + { + if (TransactionPool.Count == 0 || !_singleConnection) + { + conn = _provider.CreateConnection(); + conn.ConnectionString = _connectionString; + conn.Open(); + opened = true; + + TransactionPool.Add(conn, new Stack()); + CommandsPool.Add(conn, new List()); + } + else + { + conn = TransactionPool.Keys.First(); + + if (conn.State != ConnectionState.Open) + { + conn.Open(); + opened = true; + } + } + + ret = new DynamicConnection(this, conn, _singleTransaction); + } + else + ret = _tempConn; + } + + if (opened) + ExecuteInitCommands(ret); + + return ret; + } + + /// Close connection if we are allowed to. + /// Connection to manage. + internal void Close(IDbConnection connection) + { + if (connection == null) + return; + + if (!_singleConnection && connection != null && TransactionPool.ContainsKey(connection)) + { + // Close all commands + if (CommandsPool.ContainsKey(connection)) + { + var tmp = CommandsPool[connection].ToList(); + tmp.ForEach(cmd => cmd.Dispose()); + + CommandsPool[connection].Clear(); + } + + // Rollback remaining transactions + while (TransactionPool[connection].Count > 0) + { + IDbTransaction trans = TransactionPool[connection].Pop(); + trans.Rollback(); + trans.Dispose(); + } + + // Close connection + if (connection.State == ConnectionState.Open) + connection.Close(); + + // remove from pools + lock (SyncLock) + { + TransactionPool.Remove(connection); + CommandsPool.Remove(connection); + } + + // Set stamp + _poolStamp = DateTime.Now.Ticks; + + // Dispose the corpse + connection.Dispose(); + } + } + + /// Gets or sets contains commands executed when connection is opened. + public List InitCommands { get; set; } + + private void ExecuteInitCommands(IDbConnection conn) + { + if (InitCommands != null) + using (IDbCommand command = conn.CreateCommand()) + foreach (string commandText in InitCommands) + command + .SetCommand(commandText) + .ExecuteNonQuery(); + } + + #endregion Connection + + #region Transaction + + /// Begins a global database transaction. + /// Using this method connection is set to single open + /// connection until all transactions are finished. + /// Returns representation. + public IDbTransaction BeginTransaction() + { + _tempConn = Open() as DynamicConnection; + + return _tempConn.BeginTransaction(null, () => + { + var t = TransactionPool.TryGetValue(_tempConn.Connection); + + if (t == null | t.Count == 0) + { + _tempConn.Dispose(); + _tempConn = null; + } + }); + } + + #endregion Transaction + + #region IExtendedDisposable Members + + /// Performs application-defined tasks associated with freeing, + /// releasing, or resetting unmanaged resources. + public void Dispose() + { +#if !DYNAMORM_OMMIT_OLDSYNTAX + var tables = TablesCache.Values.ToList(); + TablesCache.Clear(); + + tables.ForEach(t => t.Dispose()); +#endif + + foreach (var con in TransactionPool) + { + // Close all commands + if (CommandsPool.ContainsKey(con.Key)) + { + var tmp = CommandsPool[con.Key].ToList(); + tmp.ForEach(cmd => cmd.Dispose()); + + CommandsPool[con.Key].Clear(); + } + + // Rollback remaining transactions + while (con.Value.Count > 0) + { + IDbTransaction trans = con.Value.Pop(); + trans.Rollback(); + trans.Dispose(); + } + + // Close connection + if (con.Key.State == ConnectionState.Open) + con.Key.Close(); + + // Dispose it + con.Key.Dispose(); + } + + // Clear pools + lock (SyncLock) + { + TransactionPool.Clear(); + CommandsPool.Clear(); + } + + IsDisposed = true; + } + + /// Gets a value indicating whether this instance is disposed. + public bool IsDisposed { get; private set; } + + #endregion IExtendedDisposable Members + } + + /// Represents database connection options. + [Flags] + [System.Reflection.ObfuscationAttribute(Feature = "renaming", ApplyToMembers = true)] + public enum DynamicDatabaseOptions + { + /// No specific options. + None = 0x00000000, + + /// Only single persistent database connection. + SingleConnection = 0x00000001, + + /// Only one transaction. + SingleTransaction = 0x00000002, + + /// Database supports top syntax (SELECT TOP x ... FROM ...). + SupportTop = 0x00000080, + + /// Database supports limit offset syntax (SELECT ... FROM ... LIMIT x OFFSET y). + SupportLimitOffset = 0x00000040, + + /// Database support standard schema. + SupportSchema = 0x00000010, + + /// Database support stored procedures (EXEC procedure ...). + SupportStoredProcedures = 0x00000020, + + /// Debug option allowing to enable command dumps by default. + DumpCommands = 0x01000000, + } + + /// Extension to ORM objects. + public static class DynamicExtensions + { + #region Type column map + + /// MapEnumerable of .NET types to . + public static readonly Dictionary TypeMap = new Dictionary() + { + { typeof(byte), DbType.Byte }, + { typeof(sbyte), DbType.SByte }, + { typeof(short), DbType.Int16 }, + { typeof(ushort), DbType.UInt16 }, + { typeof(int), DbType.Int32 }, + { typeof(uint), DbType.UInt32 }, + { typeof(long), DbType.Int64 }, + { typeof(ulong), DbType.UInt64 }, + { typeof(float), DbType.Single }, + { typeof(double), DbType.Double }, + { typeof(decimal), DbType.Decimal }, + { typeof(bool), DbType.Boolean }, + { typeof(string), DbType.String }, + { typeof(char), DbType.StringFixedLength }, + { typeof(Guid), DbType.Guid }, + { typeof(DateTime), DbType.DateTime }, + { typeof(DateTimeOffset), DbType.DateTimeOffset }, + { typeof(byte[]), DbType.Binary }, + { typeof(byte?), DbType.Byte }, + { typeof(sbyte?), DbType.SByte }, + { typeof(short?), DbType.Int16 }, + { typeof(ushort?), DbType.UInt16 }, + { typeof(int?), DbType.Int32 }, + { typeof(uint?), DbType.UInt32 }, + { typeof(long?), DbType.Int64 }, + { typeof(ulong?), DbType.UInt64 }, + { typeof(float?), DbType.Single }, + { typeof(double?), DbType.Double }, + { typeof(decimal?), DbType.Decimal }, + { typeof(bool?), DbType.Boolean }, + { typeof(char?), DbType.StringFixedLength }, + { typeof(Guid?), DbType.Guid }, + { typeof(DateTime?), DbType.DateTime }, + { typeof(DateTimeOffset?), DbType.DateTimeOffset } + }; + + #endregion Type column map + + #region Command extensions + + /// Set connection on the fly. + /// in which changes will be made. + /// which will be set to instance. + /// Returns edited instance. + public static IDbCommand SetConnection(this IDbCommand command, IDbConnection connection) + { + command.Connection = connection; + + return command; + } + + /// Set connection on the fly. + /// in which changes will be made. + /// which will be set to instance. + /// Returns edited instance. + public static IDbCommand SetTransaction(this IDbCommand command, IDbTransaction transaction) + { + command.Transaction = transaction; + + return command; + } + + #region SetCommand + + /// Set properties on the fly. + /// in which changes will be made. + /// Indicates or specifies how the property is interpreted. + /// The wait time before terminating the attempt to execute a command and generating an error. + /// The text command to run against the data source. + /// Arguments used to format command. + /// Returns edited instance. + public static IDbCommand SetCommand(this IDbCommand command, CommandType commandType, int commandTimeout, string commandText, params object[] args) + { + command.CommandType = commandType; + command.CommandTimeout = commandTimeout; + + if (args != null && args.Length > 0) + command.CommandText = string.Format(commandText, args); + else + command.CommandText = commandText; + + return command; + } + + /// Set properties on the fly. + /// in which changes will be made. + /// The wait time before terminating the attempt to execute a command and generating an error. + /// The text command to run against the data source. + /// Arguments used to format command. + /// Returns edited instance. + public static IDbCommand SetCommand(this IDbCommand command, int commandTimeout, string commandText, params object[] args) + { + command.CommandTimeout = commandTimeout; + + if (args != null && args.Length > 0) + command.CommandText = string.Format(commandText, args); + else + command.CommandText = commandText; + + return command; + } + + /// Set properties on the fly. + /// in which changes will be made. + /// Indicates or specifies how the property is interpreted. + /// The text command to run against the data source. + /// Arguments used to format command. + /// Returns edited instance. + public static IDbCommand SetCommand(this IDbCommand command, CommandType commandType, string commandText, params object[] args) + { + command.CommandType = commandType; + + if (args != null && args.Length > 0) + command.CommandText = string.Format(commandText, args); + else + command.CommandText = commandText; + + return command; + } + + /// Set properties on the fly. + /// in which changes will be made. + /// The text command to run against the data source. + /// Arguments used to format command. + /// Returns edited instance. + public static IDbCommand SetCommand(this IDbCommand command, string commandText, params object[] args) + { + if (args != null && args.Length > 0) + command.CommandText = string.Format(commandText, args); + else + command.CommandText = commandText; + + return command; + } + + /// Set properties on the fly. + /// in which changes will be made. + /// Command builder. + /// Returns edited instance. + public static IDbCommand SetCommand(this IDbCommand command, IDynamicQueryBuilder builder) + { + builder.FillCommand(command); + + return command; + } + + #endregion SetCommand + + #region AddParameter + + /// Extension method for adding in a bunch of parameters. + /// Command to handle. + /// Database object required to get proper formatting. + /// Items to add. + /// Returns edited instance. + public static IDbCommand AddParameters(this IDbCommand cmd, DynamicDatabase database, params object[] args) + { + if (args != null && args.Count() > 0) + foreach (var item in args) + cmd.AddParameter(database, item); + + return cmd; + } + + /// Extension method for adding in a bunch of parameters. + /// Command to handle. + /// Database object required to get proper formatting. + /// Items to add in an expando object. + /// Returns edited instance. + public static IDbCommand AddParameters(this IDbCommand cmd, DynamicDatabase database, ExpandoObject args) + { + if (args != null && args.Count() > 0) + foreach (var item in args.ToDictionary()) + cmd.AddParameter(database, item.Key, item.Value); + + return cmd; + } + + /// Extension for adding single parameter determining only type of object. + /// Command to handle. + /// Database object required to get proper formatting. + /// Items to add. + /// Returns edited instance. + public static IDbCommand AddParameter(this IDbCommand cmd, DynamicDatabase database, object item) + { + return cmd.AddParameter(database, database.GetParameterName(cmd.Parameters.Count), item); + } + + /// Extension for adding single parameter determining only type of object. + /// Command to handle. + /// Database object required to get proper formatting. + /// Name of parameter. + /// Items to add. + /// Returns edited instance. + public static IDbCommand AddParameter(this IDbCommand cmd, DynamicDatabase database, string name, object item) + { + var p = cmd.CreateParameter(); + p.ParameterName = name; + + if (item == null || item == DBNull.Value) + p.Value = DBNull.Value; + else + { + Type type = item.GetType(); + + p.DbType = TypeMap.TryGetNullable(type) ?? DbType.String; + + if (type == typeof(ExpandoObject)) + p.Value = ((IDictionary)item).Values.FirstOrDefault(); + else + p.Value = item; + + if (p.DbType == DbType.String) + p.Size = item.ToString().Length > 4000 ? -1 : 4000; + } + + cmd.Parameters.Add(p); + + return cmd; + } + + /// Extension for adding single parameter determining only type of object. + /// Command to handle. + /// Query builder containing schema. + /// Column schema to use. + /// Parameter value. + /// Returns edited instance. + public static IDbCommand AddParameter(this IDbCommand cmd, IDynamicQueryBuilder builder, DynamicSchemaColumn? col, object value) + { + var p = cmd.CreateParameter(); + p.ParameterName = builder.Database.GetParameterName(cmd.Parameters.Count); + + if (col.HasValue) + { + p.DbType = col.Value.Type; + + if ((builder.Database.Options & DynamicDatabaseOptions.SupportSchema) == DynamicDatabaseOptions.SupportSchema) + { + p.Size = col.Value.Size; + p.Precision = col.Value.Precision; + p.Scale = col.Value.Scale; + + // Quick fix - review that + // Quick fix 2 - use item.Schema in that case + if (p.Scale > p.Precision) + p.Scale = 4; + } + + p.Value = value == null ? DBNull.Value : value; + } + else if (value == null || value == DBNull.Value) + p.Value = DBNull.Value; + else + { + p.DbType = TypeMap.TryGetNullable(value.GetType()) ?? DbType.String; + + if (p.DbType == DbType.String) + p.Size = value.ToString().Length > 4000 ? -1 : 4000; + + p.Value = value; + } + + cmd.Parameters.Add(p); + + return cmd; + } + + /// Extension for adding single parameter determining only type of object. + /// Command to handle. + /// Query builder containing schema. + /// Column item to add. + /// Returns edited instance. + public static IDbCommand AddParameter(this IDbCommand cmd, IDynamicQueryBuilder builder, DynamicColumn item) + { + var p = cmd.CreateParameter(); + p.ParameterName = builder.Database.GetParameterName(cmd.Parameters.Count); + + var col = item.Schema ?? (builder as DynamicQueryBuilder) + .NullOr(b => b.GetColumnFromSchema(item.ColumnName), + builder.Tables.FirstOrDefault() + .NullOr(t => t.Schema + .NullOr(s => s.TryGetNullable(item.ColumnName.ToLower()), null), null)); + + if (col.HasValue) + { + p.DbType = col.Value.Type; + + if (builder.SupportSchema) + { + p.Size = col.Value.Size; + p.Precision = col.Value.Precision; + p.Scale = col.Value.Scale; + + // Quick fix - review that + // Quick fix 2 - use item.Schema in that case + if (p.Scale > p.Precision) + p.Scale = 4; + } + + p.Value = item.Value == null ? DBNull.Value : item.Value; + } + else if (item.Value == null || item.Value == DBNull.Value) + p.Value = DBNull.Value; + else + { + p.DbType = TypeMap.TryGetNullable(item.Value.GetType()) ?? DbType.String; + + if (p.DbType == DbType.String) + p.Size = item.Value.ToString().Length > 4000 ? -1 : 4000; + + p.Value = item.Value; + } + + cmd.Parameters.Add(p); + + return cmd; + } + + /// Add to on the fly. + /// to which parameter will be added. + /// The name of the . + /// Value indicating whether the parameter is input-only, output-only, bidirectional, or a stored procedure return value . + /// The of the . + /// The size of the parameter. + /// Indicates the precision of numeric parameters. + /// Indicates the scale of numeric parameters. + /// The value of the . + /// Returns edited instance. + public static IDbCommand AddParameter(this IDbCommand command, string parameterName, ParameterDirection parameterDirection, DbType databaseType, int size, byte precision, byte scale, object value) + { + IDbDataParameter param = command.CreateParameter(); + param.ParameterName = parameterName; + param.Direction = parameterDirection; + param.DbType = databaseType; + param.Size = size; + param.Precision = precision; + param.Scale = scale; + param.Value = value; + command.Parameters.Add(param); + + return command; + } + + /// Add to on the fly. + /// to which parameter will be added. + /// The name of the . + /// Value indicating whether the parameter is input-only, output-only, bidirectional, or a stored procedure return value . + /// The of the . + /// The size of the parameter. + /// Indicates the precision of numeric parameters. + /// Indicates the scale of numeric parameters. + /// Returns edited instance. + public static IDbCommand AddParameter(this IDbCommand command, string parameterName, ParameterDirection parameterDirection, DbType databaseType, int size, byte precision, byte scale) + { + IDbDataParameter param = command.CreateParameter(); + param.ParameterName = parameterName; + param.Direction = parameterDirection; + param.DbType = databaseType; + param.Size = size; + param.Precision = precision; + param.Scale = scale; + command.Parameters.Add(param); + + return command; + } + + /// Add to on the fly. + /// to which parameter will be added. + /// The name of the . + /// Value indicating whether the parameter is input-only, output-only, bidirectional, or a stored procedure return value . + /// The of the . + /// Indicates the precision of numeric parameters. + /// Indicates the scale of numeric parameters. + /// The value of the . + /// Returns edited instance. + public static IDbCommand AddParameter(this IDbCommand command, string parameterName, ParameterDirection parameterDirection, DbType databaseType, byte precision, byte scale, object value) + { + IDbDataParameter param = command.CreateParameter(); + param.ParameterName = parameterName; + param.Direction = parameterDirection; + param.DbType = databaseType; + param.Precision = precision; + param.Scale = scale; + param.Value = value; + command.Parameters.Add(param); + + return command; + } + + /// Add to on the fly. + /// to which parameter will be added. + /// The name of the . + /// The of the . + /// Indicates the precision of numeric parameters. + /// Indicates the scale of numeric parameters. + /// The value of the . + /// Returns edited instance. + public static IDbCommand AddParameter(this IDbCommand command, string parameterName, DbType databaseType, byte precision, byte scale, object value) + { + IDbDataParameter param = command.CreateParameter(); + param.ParameterName = parameterName; + param.DbType = databaseType; + param.Precision = precision; + param.Scale = scale; + param.Value = value; + command.Parameters.Add(param); + + return command; + } + + /// Add to on the fly. + /// to which parameter will be added. + /// The name of the . + /// The of the . + /// Indicates the precision of numeric parameters. + /// Indicates the scale of numeric parameters. + /// Returns edited instance. + public static IDbCommand AddParameter(this IDbCommand command, string parameterName, DbType databaseType, byte precision, byte scale) + { + IDbDataParameter param = command.CreateParameter(); + param.ParameterName = parameterName; + param.DbType = databaseType; + param.Precision = precision; + param.Scale = scale; + command.Parameters.Add(param); + + return command; + } + + /// Add to on the fly. + /// to which parameter will be added. + /// The name of the . + /// Value indicating whether the parameter is input-only, output-only, bidirectional, or a stored procedure return value . + /// The of the . + /// The size of the parameter. + /// The value of the . + /// Returns edited instance. + public static IDbCommand AddParameter(this IDbCommand command, string parameterName, ParameterDirection parameterDirection, DbType databaseType, int size, object value) + { + IDbDataParameter param = command.CreateParameter(); + param.ParameterName = parameterName; + param.Direction = parameterDirection; + param.DbType = databaseType; + param.Size = size; + param.Value = value; + command.Parameters.Add(param); + + return command; + } + + /// Add to on the fly. + /// to which parameter will be added. + /// The name of the . + /// The of the . + /// The size of the parameter. + /// The value of the . + /// Returns edited instance. + public static IDbCommand AddParameter(this IDbCommand command, string parameterName, DbType databaseType, int size, object value) + { + IDbDataParameter param = command.CreateParameter(); + param.ParameterName = parameterName; + param.DbType = databaseType; + param.Size = size; + param.Value = value; + command.Parameters.Add(param); + + return command; + } + + /// Add to on the fly. + /// to which parameter will be added. + /// The name of the . + /// The of the . + /// The value of the . + /// Returns edited instance. + public static IDbCommand AddParameter(this IDbCommand command, string parameterName, DbType databaseType, object value) + { + IDbDataParameter param = command.CreateParameter(); + param.ParameterName = parameterName; + param.DbType = databaseType; + param.Value = value; + command.Parameters.Add(param); + + return command; + } + + /// Add to on the fly. + /// to which parameter will be added. + /// The name of the . + /// The of the . + /// The size of the parameter. + /// Returns edited instance. + public static IDbCommand AddParameter(this IDbCommand command, string parameterName, DbType databaseType, int size) + { + IDbDataParameter param = command.CreateParameter(); + param.ParameterName = parameterName; + param.DbType = databaseType; + param.Size = size; + command.Parameters.Add(param); + + return command; + } + + /// Add to on the fly. + /// to which parameter will be added. + /// The name of the . + /// The of the . + /// Returns edited instance. + public static IDbCommand AddParameter(this IDbCommand command, string parameterName, DbType databaseType) + { + IDbDataParameter param = command.CreateParameter(); + param.ParameterName = parameterName; + param.DbType = databaseType; + command.Parameters.Add(param); + + return command; + } + + #endregion AddParameter + + #region SetParameter + + /// Set value for on the fly. + /// to which parameter will be added. + /// The name of the . + /// Value to set on this parameter. + /// Returns edited instance. + public static IDbCommand SetParameter(this IDbCommand command, string parameterName, object value) + { + ((IDbDataParameter)command.Parameters[parameterName]).Value = value; + + return command; + } + + /// Set value for on the fly. + /// to which parameter will be added. + /// Index of the . + /// Value to set on this parameter. + /// Returns edited instance. + public static IDbCommand SetParameter(this IDbCommand command, int index, object value) + { + ((IDbDataParameter)command.Parameters[index]).Value = value; + + return command; + } + + #endregion SetParameter + + #region Generic Execution + +#if !DYNAMORM_OMMIT_GENERICEXECUTION && !DYNAMORM_OMMIT_TRYPARSE + + /// Execute scalar and return string if possible. + /// Type to parse to. + /// which will be executed. + /// Returns resulting instance of T from query. + public static T ExecuteScalarAs(this IDbCommand command) + { + return ExecuteScalarAs(command, default(T), null); + } + + /// Execute scalar and return string if possible. + /// Type to parse to. + /// which will be executed. + /// Handler of a try parse method. + /// Returns resulting instance of T from query. + public static T ExecuteScalarAs(this IDbCommand command, DynamicExtensions.TryParseHandler handler) + { + return ExecuteScalarAs(command, default(T), handler); + } + + /// Execute scalar and return string if possible. + /// Type to parse to. + /// which will be executed. + /// Default result value. + /// Returns resulting instance of T from query. + public static T ExecuteScalarAs(this IDbCommand command, T defaultValue) + { + return ExecuteScalarAs(command, defaultValue, null); + } + + /// Execute scalar and return string if possible. + /// Type to parse to. + /// which will be executed. + /// Default result value. + /// Handler of a try parse method. + /// Returns resulting instance of T from query. + public static T ExecuteScalarAs(this IDbCommand command, T defaultValue, DynamicExtensions.TryParseHandler handler) + { + T ret = defaultValue; + + object o = command.ExecuteScalar(); + + if (o is T) + return (T)o; + else if (o != DBNull.Value && o != null) + { + var method = typeof(T).GetMethod( + "TryParse", + new[] + { + typeof(string), + Type.GetType(string.Format("{0}&", typeof(T).FullName)) + }); + + if (handler != null) + ret = o.ToString().TryParseDefault(defaultValue, handler); + else if (method != null) + ret = o.ToString().TryParseDefault(defaultValue, delegate(string v, out T r) + { + r = defaultValue; + return (bool)method.Invoke(null, new object[] { v, r }); + }); + else if (typeof(T) == typeof(string)) + ret = (T)(o.ToString() as object); + else if (typeof(T) == typeof(object)) + ret = (T)o; + else + throw new InvalidOperationException("Provided type can't be parsed using generic approach."); + } + + return ret; + } + + /// Execute enumerator of specified type. + /// Type to parse to. + /// which will be executed. + /// Default result value. + /// Handler of a try parse method. + /// Returns enumerator of specified type from query. + public static IEnumerable ExecuteEnumeratorOf(this IDbCommand command, T defaultValue, DynamicExtensions.TryParseHandler handler) where T : struct + { + using (IDataReader reader = command.ExecuteReader()) + { + var method = typeof(T).GetMethod( + "TryParse", + new[] + { + typeof(string), + Type.GetType(string.Format("{0}&", typeof(T).FullName)) + }); + + while (reader.Read()) + { + T ret = defaultValue; + + if (!reader.IsDBNull(0)) + { + object o = reader.GetValue(0); + + if (o is T) + ret = (T)o; + else if (o != DBNull.Value) + { + if (handler != null) + ret = o.ToString().TryParseDefault(defaultValue, handler); + else if (method != null) + ret = o.ToString().TryParseDefault(defaultValue, delegate(string v, out T r) + { + r = defaultValue; + return (bool)method.Invoke(null, new object[] { v, r }); + }); + else if (typeof(T) == typeof(string)) + ret = (T)(o.ToString() as object); + else if (typeof(T) == typeof(object)) + ret = (T)o; + else + throw new InvalidOperationException("Provided type can't be parsed using generic approach."); + } + } + + yield return ret; + } + } + } + +#endif + + #endregion Generic Execution + + /// Dump command into text writer. + /// Command to dump. + /// Builder to which write output. + /// Returns dumped instance. + public static IDbCommand Dump(this IDbCommand command, StringBuilder buider) + { + using (StringWriter sw = new StringWriter(buider)) + return command.Dump(sw); + } + + /// Dump command into text writer. + /// Command to dump. + /// Writer to which write output. + /// Returns dumped instance. + public static IDbCommand Dump(this IDbCommand command, TextWriter writer) + { + writer.WriteLine("Type: {0}; Timeout: {1}; Query: {2}", command.CommandType, command.CommandTimeout, command.CommandText); + + if (command.Parameters.Count > 0) + { + writer.WriteLine("Parameters:"); + + foreach (IDbDataParameter param in command.Parameters) + { + writer.WriteLine(" '{0}' ({1} (s:{2} p:{3} s:{4})) = '{5}' ({6});", + param.ParameterName, + param.DbType, + param.Scale, + param.Precision, + param.Scale, + param.Value is byte[] ? ConvertByteArrayToHexString((byte[])param.Value) : param.Value ?? "NULL", + param.Value != null ? param.Value.GetType().Name : "DBNull"); + } + + writer.WriteLine(); + } + + return command; + } + + /// Convert byte array to hex formatted string without separators. + /// Byte Array Data. + /// Hex string representation of byte array. + private static string ConvertByteArrayToHexString(byte[] data) + { + return ConvertByteArrayToHexString(data, 0); + } + + /// Convert byte array to hex formatted string. + /// Byte Array Data. + /// Put '-' each separatorEach characters. + /// Hex string representation of byte array. + private static string ConvertByteArrayToHexString(byte[] data, int separatorEach) + { + int len = data.Length * 2; + System.Text.StringBuilder sb = new System.Text.StringBuilder(); + for (int i = 0; i < len; i++) + { + sb.AppendFormat("{0:X}", data[(i / 2)] >> (((i % 2) == 0) ? 4 : 0) & 0x0F); + if ((separatorEach > 0) && ((i + 1) % separatorEach == 0)) + sb.AppendFormat("-"); + } + + return sb.ToString(); + } + + #endregion Command extensions + + #region Dynamic builders extensions + + /// Turns an to a Dynamic list of things. + /// Ready to execute builder. + /// List of things. + public static List ToList(this IDynamicSelectQueryBuilder b) + { + return b.Execute().ToList(); + } + + /// Sets the on create temporary parameter action. + /// Class implementing interface. + /// The builder on which set delegate. + /// Action to invoke. + /// Returns instance of builder on which action is set. + public static T CreateTemporaryParameterAction(this T b, Action a) where T : IDynamicQueryBuilder + { + b.OnCreateTemporaryParameter = a; + return b; + } + + /// Sets the on create real parameter action. + /// Class implementing interface. + /// The builder on which set delegate. + /// Action to invoke. + /// Returns instance of builder on which action is set. + public static T CreateParameterAction(this T b, Action a) where T : IDynamicQueryBuilder + { + b.OnCreateParameter = a; + return b; + } + + /// Sets the virtual mode on builder. + /// Class implementing interface. + /// The builder on which set virtual mode. + /// Virtual mode. + /// Returns instance of builder on which virtual mode is set. + public static T SetVirtualMode(this T b, bool virtualMode) where T : IDynamicQueryBuilder + { + b.VirtualMode = virtualMode; + return b; + } + + /// Creates sub query that can be used inside of from/join/expressions. + /// Class implementing interface. + /// The builder that will be parent of new sub query. + /// Instance of sub query. + public static IDynamicSelectQueryBuilder SubQuery(this T b) where T : IDynamicQueryBuilder + { + return new DynamicSelectQueryBuilder(b.Database, b as DynamicQueryBuilder); + } + + /// Creates sub query that can be used inside of from/join/expressions. + /// Class implementing interface. + /// The builder that will be parent of new sub query. + /// The specification for sub query. + /// The specification for sub query. + /// Instance of sub query. + public static IDynamicSelectQueryBuilder SubQuery(this T b, Func fn, params Func[] func) where T : IDynamicQueryBuilder + { + return new DynamicSelectQueryBuilder(b.Database, b as DynamicQueryBuilder).From(fn, func); + } + + /// Creates sub query that can be used inside of from/join/expressions. + /// Class implementing interface. + /// The builder that will be parent of new sub query. + /// First argument is parent query, second one is a sub query. + /// This instance to permit chaining. + public static T SubQuery(this T b, Action subquery) where T : IDynamicQueryBuilder + { + var sub = b.SubQuery(); + + subquery(b, sub); + + (b as DynamicQueryBuilder).ParseCommand(sub as DynamicQueryBuilder, b.Parameters); + + return b; + } + + /// Creates sub query that can be used inside of from/join/expressions. + /// Class implementing interface. + /// The builder that will be parent of new sub query. + /// First argument is parent query, second one is a sub query. + /// The specification for sub query. + /// The specification for sub query. + /// This instance to permit chaining. + public static T SubQuery(this T b, Action subquery, Func fn, params Func[] func) where T : IDynamicQueryBuilder + { + var sub = b.SubQuery(fn, func); + + subquery(b, sub); + + (b as DynamicQueryBuilder).ParseCommand(sub as DynamicQueryBuilder, b.Parameters); + + return b; + } + + #endregion Dynamic builders extensions + + #region Dynamic extensions + + /// Turns an to a Dynamic list of things. + /// Reader from which read data. + /// List of things. + public static List ToList(this IDataReader r) + { + var result = new List(); + + while (r.Read()) + result.Add(r.RowToDynamic()); + + return result; + } + + /// Turns an to a Dynamic list of things with specified type. + /// Type of object to map on. + /// Ready to execute builder. + /// List of things. + public static List ToList(this IDynamicSelectQueryBuilder b) where T : class + { + return b.Execute().ToList(); + } + + /// Turns the dictionary into an ExpandoObject. + /// Dictionary to convert. + /// Converted dictionary. + public static dynamic ToDynamic(this IDictionary d) + { + var result = new ExpandoObject(); + var dict = result as IDictionary; + + foreach (var prop in d) + dict.Add(prop.Key, prop.Value); + + return result; + } + + /// Turns the object into an ExpandoObject. + /// Object to convert. + /// Converted object. + public static dynamic ToDynamic(this object o) + { + var result = new ExpandoObject(); + var dict = result as IDictionary; + var ot = o.GetType(); + + if (ot == typeof(ExpandoObject)) + return o; + + if (o is IDictionary) + ((IDictionary)o) + .ToList() + .ForEach(kvp => dict.Add(kvp.Key, kvp.Value)); + else if (ot == typeof(NameValueCollection) || ot.IsSubclassOf(typeof(NameValueCollection))) + { + var nameValue = (NameValueCollection)o; + nameValue.Cast() + .Select(key => new KeyValuePair(key, nameValue[key])) + .ToList() + .ForEach(i => dict.Add(i)); + } + else + { + var mapper = DynamicMapperCache.GetMapper(ot); + + if (mapper != null) + { + foreach (var item in mapper.ColumnsMap.Values) + if (item.Get != null) + dict.Add(item.Name, item.Get(o)); + } + else + { + var props = ot.GetProperties(); + + foreach (var item in props) + if (item.CanRead) + dict.Add(item.Name, item.GetValue(o, null)); + } + } + + return result; + } + + /// Convert data row row into dynamic object. + /// DataRow from which read. + /// Generated dynamic object. + public static dynamic RowToDynamic(this DataRow r) + { + dynamic e = new ExpandoObject(); + var d = e as IDictionary; + + int len = r.Table.Columns.Count; + + for (int i = 0; i < len; i++) + d.Add(r.Table.Columns[i].ColumnName, r.IsNull(i) ? null : r[i]); + + return e; + } + + /// Convert data row row into dynamic object (upper case key). + /// DataRow from which read. + /// Generated dynamic object. + public static dynamic RowToDynamicUpper(this DataRow r) + { + dynamic e = new ExpandoObject(); + var d = e as IDictionary; + + int len = r.Table.Columns.Count; + + for (int i = 0; i < len; i++) + d.Add(r.Table.Columns[i].ColumnName.ToUpper(), r.IsNull(i) ? null : r[i]); + + return e; + } + + /// Convert reader row into dynamic object. + /// Reader from which read. + /// Generated dynamic object. + public static dynamic RowToDynamic(this IDataReader r) + { + dynamic e = new ExpandoObject(); + var d = e as IDictionary; + + int c = r.FieldCount; + for (int i = 0; i < c; i++) + try + { + d.Add(r.GetName(i), r.IsDBNull(i) ? null : r[i]); + } + catch (ArgumentException argex) + { + throw new ArgumentException( + string.Format("Field '{0}' is defined more than once in a query.", r.GetName(i)), "Column name or alias", argex); + } + + return e; + } + + /// Turns the object into a Dictionary. + /// Object to convert. + /// Resulting dictionary. + public static IDictionary ToDictionary(this ExpandoObject o) + { + return (IDictionary)o; + } + + /// Turns the object into a Dictionary. + /// Object to convert. + /// Resulting dictionary. + public static IDictionary ToDictionary(this object o) + { + return o is IDictionary ? + (IDictionary)o : + (IDictionary)o.ToDynamic(); + } + + #endregion Dynamic extensions + + #region Type extensions + + /// Check if type is anonymous. + /// Type to test. + /// Returns true if type is anonymous. + public static bool IsAnonymous(this Type type) + { + // HACK: The only way to detect anonymous types right now. + return Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false) + && type.IsGenericType + && (type.Name.Contains("AnonymousType") || type.Name.Contains("AnonType")) + && (type.Name.StartsWith("<>") || type.Name.StartsWith("VB$")) + && (type.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic; + } + + /// Check if type implements IEnumerable<> interface. + /// Type to check. + /// Returns true if it does. + public static bool IsGenericEnumerable(this Type type) + { + return type.IsGenericType && + typeof(IEnumerable<>).IsAssignableFrom(type.GetGenericTypeDefinition()); + } + + /// Check if type implements IEnumerable<> interface. + /// Type to check. + /// Returns true if it does. + public static bool IsNullableType(this Type type) + { + Type generic = type.IsGenericType ? type.GetGenericTypeDefinition() : null; + + if (generic != null && generic.Equals(typeof(Nullable<>)) && type.IsClass) + return true; + + return false; + } + + /// Check if type is collection of any kind. + /// Type to check. + /// Returns true if it is. + public static bool IsCollection(this Type type) + { + if (!type.IsArray) + return type.IsGenericEnumerable(); + + return true; + } + + /// Check if type is collection of value types like int. + /// Type to check. + /// Returns true if it is. + public static bool IsCollectionOfValueTypes(this Type type) + { + if (type.IsArray) + return type.GetElementType().IsValueType; + else + { + if (type.IsGenericType) + { + Type[] gt = type.GetGenericArguments(); + + return (gt.Length == 1) && gt[0].IsValueType; + } + } + + return false; + } + + #endregion Type extensions + + #region IDictionary extensions + + /// Gets the value associated with the specified key. + /// The type of keys in the dictionary. + /// The type of values in the dictionary. + /// Dictionary to probe. + /// The key whose value to get. + /// Nullable type containing value or null if key was not found. + public static TValue? TryGetNullable(this IDictionary dict, TKey key) where TValue : struct + { + TValue val; + + if (key != null && dict.TryGetValue(key, out val)) + return val; + + return null; + } + + /// Gets the value associated with the specified key. + /// The type of keys in the dictionary. + /// The type of values in the dictionary. + /// Dictionary to probe. + /// The key whose value to get. + /// Instance of object or null if not found. + public static TValue TryGetValue(this IDictionary dict, TKey key) where TValue : class + { + TValue val; + + if (key != null && dict.TryGetValue(key, out val)) + return val; + + return default(TValue); + } + + /// Adds element to dictionary and returns added value. + /// The type of keys in the dictionary. + /// The type of values in the dictionary. + /// Dictionary to which add value. + /// The key under which value value will be added. + /// Value to add. + /// Instance of object or null if not found. + public static TValue AddAndPassValue(this IDictionary dict, TKey key, TValue value) + { + dict.Add(key, value); + return value; + } + + #endregion IDictionary extensions + + #region Mapper extensions + + /// MapEnumerable object enumerator into specified type. + /// Type to which columnMap results. + /// Source enumerator. + /// Enumerator of specified type. + public static IEnumerable MapEnumerable(this IEnumerable enumerable) + { + var mapper = DynamicMapperCache.GetMapper(); + + if (mapper == null) + throw new InvalidOperationException("Type can't be mapped for unknown reason."); + + foreach (var item in enumerable) + yield return (T)mapper.Create(item); + } + + /// MapEnumerable object item into specified type. + /// Type to which columnMap results. + /// Source object. + /// Item of specified type. + public static T Map(this object item) + { + var mapper = DynamicMapperCache.GetMapper(); + + if (mapper == null) + throw new InvalidOperationException("Type can't be mapped for unknown reason."); + + return (T)mapper.Create(item); + } + + /// Fill object of specified type with data from source object. + /// Type to which columnMap results. + /// Item to which columnMap data. + /// Item from which extract data. + /// Filled item. + public static T Fill(this T item, object source) + { + var mapper = DynamicMapperCache.GetMapper(); + + if (mapper == null) + throw new InvalidOperationException("Type can't be mapped for unknown reason."); + + mapper.Map(item, source); + + return item; + } + + /// MapEnumerable object enumerator into specified type. + /// Source enumerator. + /// Type to which columnMap results. + /// Enumerator of specified type. + public static IEnumerable MapEnumerable(this IEnumerable enumerable, Type type) + { + var mapper = DynamicMapperCache.GetMapper(type); + + if (mapper == null) + throw new InvalidOperationException("Type can't be mapped for unknown reason."); + + foreach (var item in enumerable) + yield return mapper.Create(item); + } + + /// MapEnumerable object item into specified type. + /// Source object. + /// Type to which columnMap results. + /// Item of specified type. + public static object Map(this object item, Type type) + { + var mapper = DynamicMapperCache.GetMapper(type); + + if (mapper == null) + throw new InvalidOperationException("Type can't be mapped for unknown reason."); + + return mapper.Create(item); + } + + /// Converts the elements of an + /// to the specified type. + /// The type to convert the elements of source to. + /// The that + /// contains the elements to be converted. + /// An enumerator that contains each element of + /// the source sequence converted to the specified type. + /// Thrown when + /// source is null. + /// An element in the + /// sequence cannot be cast to type T or enumerator + /// is not . + public static IEnumerable CastEnumerable(this object enumerator) + { + return (enumerator as System.Collections.IEnumerable).Cast(); + } + + #endregion Mapper extensions + + #region TryParse extensions + +#if !DYNAMORM_OMMIT_TRYPARSE + + /// Generic try parse. + /// Type to parse to. + /// Value to parse. + /// Handler of a try parse method. + /// Returns true if conversion was successful. + public static T? TryParse(this string value, TryParseHandler handler) where T : struct + { + if (String.IsNullOrEmpty(value)) + return null; + + T result; + + if (handler(value, out result)) + return result; + + return null; + } + + /// Generic try parse with default value. + /// Type to parse to. + /// Value to parse. + /// Default value of a result. + /// Handler of a try parse method. + /// Returns true if conversion was successful. + public static T TryParseDefault(this string value, T defaultValue, TryParseHandler handler) + { + if (String.IsNullOrEmpty(value)) + return defaultValue; + + T result; + + if (handler(value, out result)) + return result; + + return defaultValue; + } + + /// Delegate fro try parse function of a type. + /// Type which implements this function. + /// Value to parse. + /// Resulting value. + /// Returns true if conversion was successful. + public delegate bool TryParseHandler(string value, out T result); + +#endif + + #endregion TryParse extensions + + #region Coalesce - besicaly not an extensions + + /// Select first not null value. + /// Type to return. + /// Values to check. + /// First not null or default value. + public static T Coalesce(params T[] vals) where T : class + { + return vals.FirstOrDefault(v => v != null); + } + + /// Select first not null value. + /// Type to return. + /// Values to check. + /// First not null or default value. + public static T? CoalesceNullable(params T?[] vals) where T : struct + { + return vals.FirstOrDefault(v => v != null); + } + + #endregion Coalesce - besicaly not an extensions + } + + /// Dynamic query exception. + public class DynamicQueryException : Exception, ISerializable + { + /// Gets the dumped command which failed. + public string Command { get; private set; } + + /// Initializes a new instance of the + /// class. + /// The command which failed. + public DynamicQueryException(IDbCommand command = null) + : base("Error executing command.") + { + if (command != null) + { + var sb = new StringBuilder(); + command.Dump(sb); + Command = sb.ToString(); + } + } + + /// Initializes a new instance of the + /// class. + /// The message. + /// The command which failed. + public DynamicQueryException(string message, IDbCommand command = null) + : base(message) + { + SetCommand(command); + } + + /// Initializes a new instance of the + /// class. + /// The inner exception. + /// The command which failed. + public DynamicQueryException(Exception innerException, IDbCommand command = null) + : base("Error executing command.", innerException) + { + SetCommand(command); + } + + /// Initializes a new instance of the + /// class. + /// The message. + /// The inner exception. + /// The command which failed. + public DynamicQueryException(string message, Exception innerException, IDbCommand command = null) + : base(message, innerException) + { + SetCommand(command); + } + + /// Initializes a new instance of the + /// class. + /// The + /// that holds the serialized object data about the exception being thrown. + /// The + /// that contains contextual information about the source or destination. + public DynamicQueryException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + Command = info.GetString("Command"); + } + + /// When overridden in a derived class, sets the + /// + /// with information about the exception. + /// The + /// that holds the serialized object data about the exception being thrown. + /// The + /// that contains contextual information about the source or destination. + /// + /// + /// + /// + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + + if (!string.IsNullOrEmpty(Command)) + info.AddValue("Command", Command); + } + + private void SetCommand(IDbCommand command) + { + if (command != null && (!(command is DynamicCommand) || ((command is DynamicCommand) && !(command as DynamicCommand).IsDisposed))) + { + var sb = new StringBuilder(); + command.Dump(sb); + Command = sb.ToString(); + } + } + } + + /// Stores information about column from database schema. + public struct DynamicSchemaColumn + { + /// Gets or sets column name. + public string Name { get; set; } + + /// Gets or sets column type. + public DbType Type { get; set; } + + /// Gets or sets a value indicating whether column is a key. + public bool IsKey { get; set; } + + /// Gets or sets a value indicating whether column should have unique value. + public bool IsUnique { get; set; } + + /// Gets or sets column size. + public int Size { get; set; } + + /// Gets or sets column precision. + public byte Precision { get; set; } + + /// Gets or sets column scale. + public byte Scale { get; set; } + } + +#if !DYNAMORM_OMMIT_OLDSYNTAX + + /// Dynamic table is a simple ORM using dynamic objects. + /// + /// Assume that we have a table representing Users class. + /// + /// Let's take a look at Query possibilities. Assume we want + /// to get enumerator for all records in database, mapped to our class + /// instead of dynamic type we can use following syntax. + /// Approach first. Use dynamic Query method and just set type + /// then just cast it to user class. Remember that you must cast result + /// of Queryto IEnumerable<object>. because from + /// point of view of runtime you are operating on object type. + /// (db.Table<User>().Query(type: typeof(User)) as IEnumerable<object>).Cast<User>(); + /// Second approach is similar. We ask database using dynamic + /// Query method. The difference is that we use extension method of + /// IEnumerable<object> (to which we must cast to) to map + /// object. + /// (db.Table<User>().Query(columns: "*") as IEnumerable<object>).MapEnumerable<User>(); + /// You can also use generic approach. But be careful this method is currently available thanks to framework hack. + /// (db.Table<User>().Query<User>() as IEnumerable<object>).Cast<User>() + /// Another approach uses existing methods, but still requires a + /// cast, because Query also returns dynamic object enumerator. + /// (db.Table<User>().Query().Execute() as IEnumerable<object>).MapEnumerable<User>(); + /// + /// Below you can find various invocations of dynamic and non dynamic + /// methods of this class. x variable is a class instance. + /// First various selects: + /// x.Count(columns: "id"); + /// x.Count(last: new DynamicColumn + /// { + /// Operator = DynamicColumn.CompareOperator.In, + /// Value = new object[] { "Hendricks", "Goodwin", "Freeman" }.Take(3) + /// }); + /// x.Count(last: new DynamicColumn + /// { + /// Operator = DynamicColumn.CompareOperator.In, + /// Value = new object[] { "Hendricks", "Goodwin", "Freeman" } + /// }); + /// x.First(columns: "id").id; + /// x.Last(columns: "id").id; + /// x.Count(first: "Ori"); + /// x.Min(columns: "id"); + /// x.Max(columns: "id"); + /// x.Avg(columns: "id"); + /// x.Sum(columns: "id"); + /// x.Scalar(columns: "first", id: 19); + /// x.Scalar(columns: "first:first:group_concat", id: new DynamicColumn { Operator = DynamicColumn.CompareOperator.Lt, Value = 20 }); + /// x.Scalar(columns: "group_concat(first):first", id: new DynamicColumn { Operator = DynamicColumn.CompareOperator.Lt, Value = 20 }); + /// var v = (x.Query(columns: "first,first:occurs:count", group: "first", order: ":desc:2") as IEnumerable<dynamic>).ToList(); + /// x.Scalar(columns: @"length(""login""):len:avg"); + /// x.Avg(columns: @"length(""email""):len"); + /// x.Count(condition1: + /// new DynamicColumn() + /// { + /// ColumnName = "email", + /// Aggregate = "length", + /// Operator = DynamicColumn.CompareOperator.Gt, + /// Value = 27 + /// }); + /// var o = x.Single(columns: "id,first,last", id: 19); + /// x.Single(where: new DynamicColumn("id").Eq(100)).login; + /// x.Count(where: new DynamicColumn("id").Not(100)); + /// x.Single(where: new DynamicColumn("login").Like("Hoyt.%")).id; + /// x.Count(where: new DynamicColumn("login").NotLike("Hoyt.%")); + /// x.Count(where: new DynamicColumn("id").Greater(100)); + /// x.Count(where: new DynamicColumn("id").GreaterOrEqual(100)); + /// x.Count(where: new DynamicColumn("id").Less(100)); + /// x.Count(where: new DynamicColumn("id").LessOrEqual(100)); + /// x.Count(where: new DynamicColumn("id").Between(75, 100)); + /// x.Count(where: new DynamicColumn("id").In(75, 99, 100)); + /// x.Count(where: new DynamicColumn("id").In(new[] { 75, 99, 100 })); + /// Inserts: + /// x.Insert(code: 201, first: "Juri", last: "Gagarin", email: "juri.gagarin@megacorp.com", quote: "bla, bla, bla"); + /// x.Insert(values: new { code = 202, first = "Juri", last = "Gagarin", email = "juri.gagarin@megacorp.com", quote = "bla, bla, bla" }); + /// x.Insert(values: new Users + /// { + /// Id = u.Max(columns: "id") + 1, + /// Code = "203", + /// First = "Juri", + /// Last = "Gagarin", + /// Email = "juri.gagarin@megacorp.com", + /// Quote = "bla, bla, bla" + /// }); + /// x.Insert(values: new users + /// { + /// id = u.Max(columns: "id") + 1, + /// code = "204", + /// first = "Juri", + /// last = "Gagarin", + /// email = "juri.gagarin@megacorp.com", + /// quote = "bla, bla, bla" + /// }); + /// x.Update(id: 1, code: 201, first: "Juri", last: "Gagarin", email: "juri.gagarin@megacorp.com", quote: "bla, bla, bla"); + /// x.Update(update: new { id = 2, code = 202, first = "Juri", last = "Gagarin", email = "juri.gagarin@megacorp.com", quote = "bla, bla, bla" }); + /// Updates: + /// x.Update(update: new Users + /// { + /// Id = 3, + /// Code = "203", + /// First = "Juri", + /// Last = "Gagarin", + /// Email = "juri.gagarin@megacorp.com", + /// Quote = "bla, bla, bla" + /// }); + /// x.Update(update: new users + /// { + /// id = 4, + /// code = "204", + /// first = "Juri", + /// last = "Gagarin", + /// email = "juri.gagarin@megacorp.com", + /// quote = "bla, bla, bla" + /// }); + /// x.Update(values: new { code = 205, first = "Juri", last = "Gagarin", email = "juri.gagarin@megacorp.com", quote = "bla, bla, bla" }, where: new { id = 5 }); + /// x.Update(values: new Users + /// { + /// Id = 6, + /// Code = "206", + /// First = "Juri", + /// Last = "Gagarin", + /// Email = "juri.gagarin@megacorp.com", + /// Quote = "bla, bla, bla" + /// }, id: 6); + /// x.Update(values: new users + /// { + /// id = 7, + /// code = "207", + /// first = "Juri", + /// last = "Gagarin", + /// email = "juri.gagarin@megacorp.com", + /// quote = "bla, bla, bla" + /// }, id: 7); + /// Delete: + /// x.Delete(code: 10); + /// x.Delete(delete: new { id = 11, code = 11, first = "Juri", last = "Gagarin", email = "juri.gagarin@megacorp.com", quote = "bla, bla, bla" }); + /// x.Delete(delete: new Users + /// { + /// Id = 12, + /// Code = "12", + /// First = "Juri", + /// Last = "Gagarin", + /// Email = "juri.gagarin@megacorp.com", + /// Quote = "bla, bla, bla" + /// }); + /// x.Delete(delete: new users + /// { + /// id = 13, + /// code = "13", + /// first = "Juri", + /// last = "Gagarin", + /// email = "juri.gagarin@megacorp.com", + /// quote = "bla, bla, bla" + /// }); + /// x.Delete(where: new { id = 14, code = 14 }); + /// + public class DynamicTable : DynamicObject, IExtendedDisposable, ICloneable + { + private static HashSet _allowedCommands = new HashSet + { + "Insert", "Update", "Delete", + "Query", "Single", "Where", + "First", "Last", "Get", + "Count", "Sum", "Avg", + "Min", "Max", "Scalar" + }; + + /// Gets dynamic database. + internal DynamicDatabase Database { get; private set; } + + /// Gets type of table (for coning and schema building). + internal Type TableType { get; private set; } + + /// Gets name of table. + public virtual string TableName { get; private set; } + + /// Gets name of owner. + public virtual string OwnerName { get; private set; } + + /// Gets full name of table containing owner and table name. + public virtual string FullName + { + get + { + return string.IsNullOrEmpty(TableName) ? null : string.IsNullOrEmpty(OwnerName) ? + Database.DecorateName(TableName) : + string.Format("{0}.{1}", Database.DecorateName(OwnerName), Database.DecorateName(TableName)); + } + } + + /// Gets table schema. + /// If database doesn't support schema, only key columns are listed here. + public virtual Dictionary Schema { get; private set; } + + private DynamicTable() + { + } + + /// Initializes a new instance of the class. + /// Database and connection management. + /// Table name. + /// Owner of the table. + /// Override keys in schema. + public DynamicTable(DynamicDatabase database, string table = "", string owner = "", string[] keys = null) + { + IsDisposed = false; + Database = database; + TableName = Database.StripName(table); + OwnerName = Database.StripName(owner); + TableType = null; + + BuildAndCacheSchema(keys); + } + + /// Initializes a new instance of the class. + /// Database and connection management. + /// Type describing table. + /// Override keys in schema. + public DynamicTable(DynamicDatabase database, Type type, string[] keys = null) + { + if (type == null) + throw new ArgumentNullException("type", "Type can't be null."); + + IsDisposed = false; + + Database = database; + TableType = type; + + var mapper = DynamicMapperCache.GetMapper(type); + + if (mapper != null) + { + TableName = mapper.Table == null || string.IsNullOrEmpty(mapper.Table.Name) ? + type.Name : mapper.Table.Name; + OwnerName = mapper.Table == null || string.IsNullOrEmpty(mapper.Table.Name) ? + type.Name : mapper.Table.Name; + } + + BuildAndCacheSchema(keys); + } + + #region Schema + + private void BuildAndCacheSchema(string[] keys) + { + Dictionary schema = null; + + schema = Database.GetSchema(TableType) ?? + Database.GetSchema(TableName); + + #region Fill currrent table schema + + if (keys == null && TableType != null) + { + var mapper = DynamicMapperCache.GetMapper(TableType); + + if (mapper != null) + { + var k = mapper.ColumnsMap.Where(p => p.Value.Column != null && p.Value.Column.IsKey).Select(p => p.Key); + if (k.Count() > 0) + keys = k.ToArray(); + } + } + + if (schema != null) + { + if (keys == null) + Schema = new Dictionary(schema); + else + { + // TODO: Make this.... nicer + List ks = keys.Select(k => k.ToLower()).ToList(); + + Schema = schema.ToDictionary(k => k.Key, (v) => + { + DynamicSchemaColumn dsc = v.Value; + dsc.IsKey = ks.Contains(v.Key); + return dsc; + }); + } + } + + #endregion Fill currrent table schema + + #region Build ad-hock schema + + if (keys != null && Schema == null) + { + Schema = keys.Select(k => k.ToLower()).ToList() + .ToDictionary(k => k, k => new DynamicSchemaColumn { Name = k, IsKey = true }); + } + + #endregion Build ad-hock schema + } + + #endregion Schema + + #region Basic Queries + + /// Enumerate the reader and yield the result. + /// SQL query containing numbered parameters in format provided by + /// methods. Also names should be formatted with + /// method. + /// Arguments (parameters). + /// Enumerator of objects expanded from query. + public virtual IEnumerable Query(string sql, params object[] args) + { + using (var con = Database.Open()) + using (var cmd = con.CreateCommand()) + using (var rdr = cmd + .SetCommand(sql, args) + .ExecuteReader()) + while (rdr.Read()) + { + dynamic val = null; + + // Work around to avoid yield being in try...catch block: + // http://stackoverflow.com/questions/346365/why-cant-yield-return-appear-inside-a-try-block-with-a-catch + try + { + val = rdr.RowToDynamic(); + } + catch (ArgumentException argex) + { + var sb = new StringBuilder(); + cmd.Dump(sb); + + throw new ArgumentException(string.Format("{0}{1}{2}", argex.Message, Environment.NewLine, sb), + argex.InnerException.NullOr(a => a, argex)); + } + + yield return val; + } + } + + /// Enumerate the reader and yield the result. + /// Command builder. + /// Enumerator of objects expanded from query. + public virtual IEnumerable Query(IDynamicQueryBuilder builder) + { + using (var con = Database.Open()) + using (var cmd = con.CreateCommand()) + using (var rdr = cmd + .SetCommand(builder) + .ExecuteReader()) + while (rdr.Read()) + { + dynamic val = null; + + // Work around to avoid yield being in try...catch block: + // http://stackoverflow.com/questions/346365/why-cant-yield-return-appear-inside-a-try-block-with-a-catch + try + { + val = rdr.RowToDynamic(); + } + catch (ArgumentException argex) + { + var sb = new StringBuilder(); + cmd.Dump(sb); + + throw new ArgumentException(string.Format("{0}{1}{2}", argex.Message, Environment.NewLine, sb), + argex.InnerException.NullOr(a => a, argex)); + } + + yield return val; + } + } + + /// Create new . + /// New instance. + public virtual IDynamicSelectQueryBuilder Query() + { + var builder = new DynamicSelectQueryBuilder(this.Database); + + var name = this.FullName; + if (!string.IsNullOrEmpty(name)) + builder.From(x => name); + + return builder; + } + + /// Returns a single result. + /// SQL query containing numbered parameters in format provided by + /// methods. Also names should be formatted with + /// method. + /// Arguments (parameters). + /// Result of a query. + public virtual object Scalar(string sql, params object[] args) + { + using (var con = Database.Open()) + using (var cmd = con.CreateCommand()) + { + return cmd + .SetCommand(sql).AddParameters(Database, args) + .ExecuteScalar(); + } + } + + /// Returns a single result. + /// Command builder. + /// Result of a query. + public virtual object Scalar(IDynamicQueryBuilder builder) + { + using (var con = Database.Open()) + using (var cmd = con.CreateCommand()) + { + return cmd + .SetCommand(builder) + .ExecuteScalar(); + } + } + + /// Execute stored procedure. + /// Name of stored procedure to execute. + /// Arguments (parameters) in form of expando object. + /// Number of affected rows. + public virtual int Procedure(string procName, ExpandoObject args = null) + { + if ((Database.Options & DynamicDatabaseOptions.SupportStoredProcedures) != DynamicDatabaseOptions.SupportStoredProcedures) + throw new InvalidOperationException("Database connection desn't support stored procedures."); + + using (var con = Database.Open()) + using (var cmd = con.CreateCommand()) + { + return cmd + .SetCommand(CommandType.StoredProcedure, procName).AddParameters(Database, args) + .ExecuteNonQuery(); + } + } + + /// Execute non query. + /// SQL query containing numbered parameters in format provided by + /// methods. Also names should be formatted with + /// method. + /// Arguments (parameters). + /// Number of affected rows. + public virtual int Execute(string sql, params object[] args) + { + using (var con = Database.Open()) + using (var cmd = con.CreateCommand()) + { + return cmd + .SetCommand(sql).AddParameters(Database, args) + .ExecuteNonQuery(); + } + } + + /// Execute non query. + /// Command builder. + /// Number of affected rows. + public virtual int Execute(IDynamicQueryBuilder builder) + { + using (var con = Database.Open()) + using (var cmd = con.CreateCommand()) + { + return cmd + .SetCommand(builder) + .ExecuteNonQuery(); + } + } + + /// Execute non query. + /// Command builders. + /// Number of affected rows. + public virtual int Execute(IDynamicQueryBuilder[] builers) + { + int ret = 0; + + using (var con = Database.Open()) + { + using (var trans = con.BeginTransaction()) + { + foreach (var builder in builers) + { + using (var cmd = con.CreateCommand()) + { + ret += cmd + .SetCommand(builder) + .ExecuteNonQuery(); + } + } + + trans.Commit(); + } + } + + return ret; + } + + #endregion Basic Queries + + #region Insert + + /// Create new . + /// New instance. + public dynamic Insert() + { + return new DynamicProxy(new DynamicInsertQueryBuilder(this.Database, this.FullName)); + } + + /// Adds a record to the database. You can pass in an Anonymous object, an , + /// A regular old POCO, or a NameValueCollection from a Request.Form or Request.QueryString. + /// Anonymous object, an , a regular old POCO, or a NameValueCollection + /// from a Request.Form or Request.QueryString, containing fields to update. + /// Number of updated rows. + public virtual int Insert(object o) + { + return Insert() + .Insert(o) + .Execute(); + } + + #endregion Insert + + #region Update + + /// Create new . + /// New instance. + public dynamic Update() + { + return new DynamicProxy(new DynamicUpdateQueryBuilder(this.Database, this.FullName)); + } + + /// Updates a record in the database. You can pass in an Anonymous object, an ExpandoObject, + /// a regular old POCO, or a NameValueCollection from a Request.Form or Request.QueryString. + /// Anonymous object, an ExpandoObject, a regular old POCO, or a NameValueCollection + /// from a Request.Form or Request.QueryString, containing fields to update. + /// Anonymous object, an , a regular old POCO, or a NameValueCollection + /// from a Request.Form or Request.QueryString, containing fields with conditions. + /// Number of updated rows. + public virtual int Update(object o, object key) + { + return Update() + .Values(o) + .Where(key) + .Execute(); + } + + /// Updates a record in the database using schema. You can pass in an Anonymous object, an ExpandoObject, + /// a regular old POCO, or a NameValueCollection from a Request.Form or Request.QueryString. + /// Anonymous object, an , a regular old POCO, or a NameValueCollection + /// from a Request.Form or Request.QueryString, containing fields to update and conditions. + /// Number of updated rows. + public virtual int Update(object o) + { + return Update() + .Update(o) + .Execute(); + } + + #endregion Update + + #region Delete + + /// Create new . + /// New instance. + public dynamic Delete() + { + return new DynamicProxy(new DynamicDeleteQueryBuilder(this.Database, this.FullName)); + } + + /// Removes a record from the database. You can pass in an Anonymous object, an , + /// A regular old POCO, or a NameValueCollection from a Request.Form or Request.QueryString. + /// Anonymous object, an , a regular old POCO, or a NameValueCollection + /// from a Request.Form or Request.QueryString, containing fields with where conditions. + /// If true use schema to determine key columns and ignore those which + /// aren't keys. + /// Number of updated rows. + public virtual int Delete(object o, bool schema = true) + { + return Delete() + .Where(o, schema) + .Execute(); + } + + #endregion Delete + + #region Universal Dynamic Invoker + + /// This is where the magic begins. + /// Binder to invoke. + /// Binder arguments. + /// Binder invoke result. + /// Returns true if invoke was performed. + public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) + { + // parse the method + var info = binder.CallInfo; + + // Get generic types + var types = binder.GetGenericTypeArguments(); + + // accepting named args only... SKEET! + if (info.ArgumentNames.Count != args.Length) + throw new InvalidOperationException("Please use named arguments for this type of query - the column name, orderby, columns, etc"); + + var op = binder.Name; + + // Avoid strange things + if (!_allowedCommands.Contains(op)) + throw new InvalidOperationException(string.Format("Dynamic method '{0}' is not supported.", op)); + + switch (op) + { + case "Insert": + result = DynamicInsert(args, info, types); + break; + + case "Update": + result = DynamicUpdate(args, info, types); + break; + + case "Delete": + result = DynamicDelete(args, info, types); + break; + + default: + result = DynamicQuery(args, info, op, types); + break; + } + + return true; + } + + private object DynamicInsert(object[] args, CallInfo info, IList types) + { + var builder = new DynamicInsertQueryBuilder(this.Database); + + if (types != null && types.Count == 1) + HandleTypeArgument(null, info, ref types, builder, 0); + + if (!string.IsNullOrEmpty(this.TableName) && builder.Tables.Count == 0) + builder.Table(this.TableName, this.Schema); + + // loop the named args - see if we have order, columns and constraints + if (info.ArgumentNames.Count > 0) + { + for (int i = 0; i < args.Length; i++) + { + var name = info.ArgumentNames[i].ToLower(); + + switch (name) + { + case "table": + if (args[i] is string) + builder.Table(args[i].ToString()); + else goto default; + break; + + case "values": + builder.Insert(args[i]); + break; + + case "type": + if (types == null || types.Count == 0) + HandleTypeArgument(args, info, ref types, builder, i); + else goto default; + break; + + default: + builder.Insert(name, args[i]); + break; + } + } + } + + // Execute + return Execute(builder); + } + + private object DynamicUpdate(object[] args, CallInfo info, IList types) + { + var builder = new DynamicUpdateQueryBuilder(this.Database); + + if (types != null && types.Count == 1) + HandleTypeArgument(null, info, ref types, builder, 0); + + if (!string.IsNullOrEmpty(this.TableName) && builder.Tables.Count == 0) + builder.Table(this.TableName, this.Schema); + + // loop the named args - see if we have order, columns and constraints + if (info.ArgumentNames.Count > 0) + { + for (int i = 0; i < args.Length; i++) + { + var name = info.ArgumentNames[i].ToLower(); + + switch (name) + { + case "table": + if (args[i] is string) + builder.Table(args[i].ToString()); + else goto default; + break; + + case "update": + builder.Update(args[i]); + break; + + case "values": + builder.Values(args[i]); + break; + + case "where": + builder.Where(args[i]); + break; + + case "type": + if (types == null || types.Count == 0) + HandleTypeArgument(args, info, ref types, builder, i); + else goto default; + break; + + default: + builder.Update(name, args[i]); + break; + } + } + } + + // Execute + return Execute(builder); + } + + private object DynamicDelete(object[] args, CallInfo info, IList types) + { + var builder = new DynamicDeleteQueryBuilder(this.Database); + + if (types != null && types.Count == 1) + HandleTypeArgument(null, info, ref types, builder, 0); + + if (!string.IsNullOrEmpty(this.TableName) && builder.Tables.Count == 0) + builder.Table(this.TableName, this.Schema); + + // loop the named args - see if we have order, columns and constraints + if (info.ArgumentNames.Count > 0) + { + for (int i = 0; i < args.Length; i++) + { + var name = info.ArgumentNames[i].ToLower(); + + switch (name) + { + case "table": + if (args[i] is string) + builder.Table(args[i].ToString()); + else goto default; + break; + + case "where": + builder.Where(args[i], false); + break; + + case "delete": + builder.Where(args[i], true); + break; + + case "type": + if (types == null || types.Count == 0) + HandleTypeArgument(args, info, ref types, builder, i); + else goto default; + break; + + default: + builder.Where(name, args[i]); + break; + } + } + } + + // Execute + return Execute(builder); + } + + private object DynamicQuery(object[] args, CallInfo info, string op, IList types) + { + object result; + var builder = new DynamicSelectQueryBuilder(this.Database); + + if (types != null && types.Count == 1) + HandleTypeArgument(null, info, ref types, builder, 0); + + if (!string.IsNullOrEmpty(this.TableName) && builder.Tables.Count == 0) + builder.From(x => this.TableName); + + // loop the named args - see if we have order, columns and constraints + if (info.ArgumentNames.Count > 0) + { + for (int i = 0; i < args.Length; i++) + { + var name = info.ArgumentNames[i].ToLower(); + + // TODO: Make this nicer + switch (name) + { + case "order": + if (args[i] is string) + builder.OrderByColumn(((string)args[i]).Split(',')); + else if (args[i] is string[]) + builder.OrderByColumn(args[i] as string); + else if (args[i] is DynamicColumn[]) + builder.OrderByColumn((DynamicColumn[])args[i]); + else if (args[i] is DynamicColumn) + builder.OrderByColumn((DynamicColumn)args[i]); + else goto default; + break; + + case "group": + if (args[i] is string) + builder.GroupByColumn(((string)args[i]).Split(',')); + else if (args[i] is string[]) + builder.GroupByColumn(args[i] as string); + else if (args[i] is DynamicColumn[]) + builder.GroupByColumn((DynamicColumn[])args[i]); + else if (args[i] is DynamicColumn) + builder.GroupByColumn((DynamicColumn)args[i]); + else goto default; + break; + + case "columns": + { + var agregate = (op == "Sum" || op == "Max" || op == "Min" || op == "Avg" || op == "Count") ? + op.ToUpper() : null; + + if (args[i] is string || args[i] is string[]) + builder.SelectColumn((args[i] as String).NullOr(s => s.Split(','), args[i] as String[]) + .Select(c => + { + var col = DynamicColumn.ParseSelectColumn(c); + if (string.IsNullOrEmpty(col.Aggregate)) + col.Aggregate = agregate; + + return col; + }).ToArray()); + else if (args[i] is DynamicColumn || args[i] is DynamicColumn[]) + builder.SelectColumn((args[i] as DynamicColumn).NullOr(c => new DynamicColumn[] { c }, args[i] as DynamicColumn[]) + .Select(c => + { + if (string.IsNullOrEmpty(c.Aggregate)) + c.Aggregate = agregate; + + return c; + }).ToArray()); + else goto default; + } + + break; + + case "where": + builder.Where(args[i]); + break; + + case "table": + if (args[i] is string) + builder.From(x => args[i].ToString()); + else goto default; + break; + + case "type": + if (types == null || types.Count == 0) + HandleTypeArgument(args, info, ref types, builder, i); + else goto default; + break; + + default: + builder.Where(name, args[i]); + break; + } + } + } + + if (op == "Count" && !builder.HasSelectColumns) + { + result = Scalar(builder.Select(x => x.Count())); + + if (result is long) + result = (int)(long)result; + } + else if (op == "Sum" || op == "Max" || + op == "Min" || op == "Avg" || op == "Count") + { + if (!builder.HasSelectColumns) + throw new InvalidOperationException("You must select one column to agregate."); + + result = Scalar(builder); + + if (op == "Count" && result is long) + result = (int)(long)result; + else if (result == DBNull.Value) + result = null; + } + else + { + // build the SQL + var justOne = op == "First" || op == "Last" || op == "Get" || op == "Single"; + + // Be sure to sort by DESC on selected columns + /*if (op == "Last") + { + if (builder.Order.Count > 0) + foreach (var o in builder.Order) + o.Order = o.Order == DynamicColumn.SortOrder.Desc ? + DynamicColumn.SortOrder.Asc : DynamicColumn.SortOrder.Desc; + }*/ + + if (justOne && !(op == "Last")) + { + if ((Database.Options & DynamicDatabaseOptions.SupportLimitOffset) == DynamicDatabaseOptions.SupportLimitOffset) + builder.Limit(1); + else if ((Database.Options & DynamicDatabaseOptions.SupportTop) == DynamicDatabaseOptions.SupportTop) + builder.Top(1); + } + + if (op == "Scalar") + { + if (!builder.HasSelectColumns) + throw new InvalidOperationException("You must select one column in scalar statement."); + + result = Scalar(builder); + } + else + { + if (justOne) + { + if (op == "Last") + result = Query(builder).LastOrDefault(); // Last record fallback + else + result = Query(builder).FirstOrDefault(); // return a single record + } + else + result = Query(builder); // return lots + + // MapEnumerable to specified result (still needs to be casted after that) + if (types != null) + { + if (types.Count == 1) + result = justOne ? + result.Map(types[0]) : + ((IEnumerable)result).MapEnumerable(types[0]); + + // TODO: Dictionaries + } + } + } + + return result; + } + + private void HandleTypeArgument(object[] args, CallInfo info, ref IList types, T builder, int i) where T : DynamicQueryBuilder + { + if (args != null) + { + if (args[i] is Type[]) + types = new List((Type[])args[i]); + else if (args[i] is Type) + types = new List(new Type[] { (Type)args[i] }); + } + + if (types != null && types.Count == 1 && !info.ArgumentNames.Any(a => a.ToLower() == "table")) + builder.Table(types[0]); + } + + #endregion Universal Dynamic Invoker + + #region IExtendedDisposable Members + + /// Performs application-defined tasks associated with + /// freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + // Lose reference but don't kill it. + if (Database != null) + { + Database.RemoveFromCache(this); + Database = null; + } + + IsDisposed = true; + } + + /// Gets a value indicating whether this instance is disposed. + public bool IsDisposed { get; private set; } + + #endregion IExtendedDisposable Members + + #region ICloneable Members + + /// Creates a new object that is a copy of the current + /// instance. + /// A new object that is a copy of this instance. + public object Clone() + { + return new DynamicTable() + { + Database = this.Database, + Schema = this.Schema, + TableName = this.TableName, + TableType = this.TableType + }; + } + + #endregion ICloneable Members + } + +#endif + + /// Helper class to easy manage transaction. + public class DynamicTransaction : IDbTransaction, IExtendedDisposable + { + private DynamicDatabase _db; + private DynamicConnection _con; + private bool _singleTransaction; + private Action _disposed; + private bool _operational = false; + + /// Initializes a new instance of the class. + /// Database connection manager. + /// Active connection. + /// Are we using single transaction mode? I so... act correctly. + /// One of the values. + /// This action is invoked when transaction is disposed. + internal DynamicTransaction(DynamicDatabase db, DynamicConnection con, bool singleTransaction, IsolationLevel? il, Action disposed) + { + _db = db; + _con = con; + _singleTransaction = singleTransaction; + _disposed = disposed; + + lock (_db.SyncLock) + { + if (!_db.TransactionPool.ContainsKey(_con.Connection)) + throw new InvalidOperationException("Can't create transaction using disposed connection."); + else if (_singleTransaction && _db.TransactionPool[_con.Connection].Count > 0) + _operational = false; + else + { + _db.TransactionPool[_con.Connection] + .Push(il.HasValue ? _con.Connection.BeginTransaction(il.Value) : _con.Connection.BeginTransaction()); + _db.PoolStamp = DateTime.Now.Ticks; + _operational = true; + } + } + } + + /// Commits the database transaction. + public void Commit() + { + lock (_db.SyncLock) + { + if (_operational) + { + var t = _db.TransactionPool.TryGetValue(_con.Connection); + + if (t != null && t.Count > 0) + { + IDbTransaction trans = t.Pop(); + + _db.PoolStamp = DateTime.Now.Ticks; + + trans.Commit(); + trans.Dispose(); + } + + _operational = false; + } + } + } + + /// Rolls back a transaction from a pending state. + public void Rollback() + { + lock (_db.SyncLock) + { + if (_operational) + { + var t = _db.TransactionPool.TryGetValue(_con.Connection); + + if (t != null && t.Count > 0) + { + IDbTransaction trans = t.Pop(); + + _db.PoolStamp = DateTime.Now.Ticks; + + trans.Rollback(); + trans.Dispose(); + } + + _operational = false; + } + } + } + + /// Gets connection object to associate with the transaction. + public IDbConnection Connection + { + get { return _con; } + } + + /// Gets for this transaction. + public IsolationLevel IsolationLevel { get; private set; } + + #region IExtendedDisposable Members + + /// Performs application-defined tasks associated with + /// freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + Rollback(); + + if (_disposed != null) + _disposed(); + } + + /// Gets a value indicating whether this instance is disposed. + public bool IsDisposed { get { return !_operational; } } + + #endregion IExtendedDisposable Members + } + + namespace Builders + { + /// Dynamic delete query builder interface. + /// This interface it publically available. Implementation should be hidden. + public interface IDynamicDeleteQueryBuilder : IDynamicQueryBuilder + { + /// Execute this builder. + /// Result of an execution.. + int Execute(); + + /// + /// Adds to the 'Where' clause the contents obtained from parsing the dynamic lambda expression given. The condition + /// is parsed to the appropriate syntax, where the specific customs virtual methods supported by the parser are used + /// as needed. + /// - If several Where() methods are chained their contents are, by default, concatenated with an 'AND' operator. + /// - The 'And()' and 'Or()' virtual method can be used to concatenate with an 'OR' or an 'AND' operator, as in: + /// 'Where( x => x.Or( condition ) )'. + /// + /// The specification. + /// This instance to permit chaining. + IDynamicDeleteQueryBuilder Where(Func func); + + /// Add where condition. + /// Condition column with operator and value. + /// Builder instance. + IDynamicDeleteQueryBuilder Where(DynamicColumn column); + + /// Add where condition. + /// Condition column. + /// Condition operator. + /// Condition value. + /// Builder instance. + IDynamicDeleteQueryBuilder Where(string column, DynamicColumn.CompareOperator op, object value); + + /// Add where condition. + /// Condition column. + /// Condition value. + /// Builder instance. + IDynamicDeleteQueryBuilder Where(string column, object value); + + /// Add where condition. + /// Set conditions as properties and values of an object. + /// If true use schema to determine key columns and ignore those which + /// aren't keys. + /// Builder instance. + IDynamicDeleteQueryBuilder Where(object conditions, bool schema = false); + } + + /// Dynamic insert query builder interface. + /// This interface it publically available. Implementation should be hidden. + public interface IDynamicInsertQueryBuilder : IDynamicQueryBuilder + { + /// Execute this builder. + /// Result of an execution.. + int Execute(); + + /// + /// Specifies the columns to insert using the dynamic lambda expressions given. Each expression correspond to one + /// column, and can: + /// - Resolve to a string, in this case a '=' must appear in the string. + /// - Resolve to a expression with the form: 'x => x.Column = Value'. + /// + /// The specifications. + /// The specifications. + /// This instance to permit chaining. + IDynamicInsertQueryBuilder Values(Func fn, params Func[] func); + + /// Add insert fields. + /// Insert column. + /// Insert value. + /// Builder instance. + IDynamicInsertQueryBuilder Insert(string column, object value); + + /// Add insert fields. + /// Set insert value as properties and values of an object. + /// Builder instance. + IDynamicInsertQueryBuilder Insert(object o); + } + + /// Dynamic query builder base interface. + /// This interface it publically available. Implementation should be hidden. + public interface IDynamicQueryBuilder + { + /// Gets instance. + DynamicDatabase Database { get; } + + /// Gets tables information. + IList Tables { get; } + + /// Gets the tables used in this builder. + IDictionary Parameters { get; } + + /// Gets or sets a value indicating whether add virtual parameters. + bool VirtualMode { get; set; } + + /// Gets a value indicating whether database supports standard schema. + bool SupportSchema { get; } + + /// Fill command with query. + /// Command to fill. + /// Filled instance of . + IDbCommand FillCommand(IDbCommand command); + + /// + /// Generates the text this command will execute against the underlying database. + /// + /// The text to execute against the underlying database. + /// This method must be override by derived classes. + string CommandText(); + + /// Gets or sets the on create temporary parameter action. + /// This is exposed to allow setting schema of column. + Action OnCreateTemporaryParameter { get; set; } + + /// Gets or sets the on create real parameter action. + /// This is exposed to allow modification of parameter. + Action OnCreateParameter { get; set; } + } + + /// Dynamic select query builder interface. + /// This interface it publically available. Implementation should be hidden. + public interface IDynamicSelectQueryBuilder : IDynamicQueryBuilder ////, IEnumerable + { + /// Execute this builder. + /// Enumerator of objects expanded from query. + IEnumerable Execute(); + + /// Execute this builder and map to given type. + /// Type of object to map on. + /// Enumerator of objects expanded from query. + IEnumerable Execute() where T : class; + + /// Execute this builder as a data reader. + /// Action containing reader. + void ExecuteDataReader(Action reader); + + /// Returns a single result. + /// Result of a query. + object Scalar(); + + #region From/Join + + /// + /// Adds to the 'From' clause the contents obtained by parsing the dynamic lambda expressions given. The supported + /// formats are: + /// - Resolve to a string: 'x => "Table AS Alias', where the alias part is optional. + /// - Resolve to an expression: 'x => x.Table.As( x.Alias )', where the alias part is optional. + /// - Generic expression: 'x => x( expression ).As( x.Alias )', where the alias part is mandatory. In this + /// case the alias is not annotated. + /// + /// The specification. + /// The specification. + /// This instance to permit chaining. + IDynamicSelectQueryBuilder From(Func fn, params Func[] func); + + /// + /// Adds to the 'Join' clause the contents obtained by parsing the dynamic lambda expressions given. The supported + /// formats are: + /// - Resolve to a string: 'x => "Table AS Alias ON Condition', where the alias part is optional. + /// - Resolve to an expression: 'x => x.Table.As( x.Alias ).On( condition )', where the alias part is optional. + /// - Generic expression: 'x => x( expression ).As( x.Alias ).On( condition )', where the alias part is mandatory. + /// In this case the alias is not annotated. + /// The expression might be prepended by a method that, in this case, is used to specify the specific join type you + /// want to perform, as in: 'x => x.Left()...". Two considerations apply: + /// - If a 'false' argument is used when no 'Join' part appears in its name, then no 'Join' suffix is added + /// with a space in between. + /// - If a 'false' argument is used when a 'Join' part does appear, then no split is performed to separate the + /// 'Join' part. + /// + /// The specification. + /// This instance to permit chaining. + IDynamicSelectQueryBuilder Join(params Func[] func); + + #endregion From/Join + + #region Where + + /// + /// Adds to the 'Where' clause the contents obtained from parsing the dynamic lambda expression given. The condition + /// is parsed to the appropriate syntax, where the specific customs virtual methods supported by the parser are used + /// as needed. + /// - If several Where() methods are chained their contents are, by default, concatenated with an 'AND' operator. + /// - The 'And()' and 'Or()' virtual method can be used to concatenate with an 'OR' or an 'AND' operator, as in: + /// 'Where( x => x.Or( condition ) )'. + /// + /// The specification. + /// This instance to permit chaining. + IDynamicSelectQueryBuilder Where(Func func); + + /// Add where condition. + /// Condition column with operator and value. + /// Builder instance. + IDynamicSelectQueryBuilder Where(DynamicColumn column); + + /// Add where condition. + /// Condition column. + /// Condition operator. + /// Condition value. + /// Builder instance. + IDynamicSelectQueryBuilder Where(string column, DynamicColumn.CompareOperator op, object value); + + /// Add where condition. + /// Condition column. + /// Condition value. + /// Builder instance. + IDynamicSelectQueryBuilder Where(string column, object value); + + /// Add where condition. + /// Set conditions as properties and values of an object. + /// If true use schema to determine key columns and ignore those which + /// aren't keys. + /// Builder instance. + IDynamicSelectQueryBuilder Where(object conditions, bool schema = false); + + #endregion Where + + #region Select + + /// + /// Adds to the 'Select' clause the contents obtained by parsing the dynamic lambda expressions given. The supported + /// formats are: + /// - Resolve to a string: 'x => "Table.Column AS Alias', where the alias part is optional. + /// - Resolve to an expression: 'x => x.Table.Column.As( x.Alias )', where the alias part is optional. + /// - Select all columns from a table: 'x => x.Table.All()'. + /// - Generic expression: 'x => x( expression ).As( x.Alias )', where the alias part is mandatory. In this case + /// the alias is not annotated. + /// + /// The specification. + /// The specification. + /// This instance to permit chaining. + IDynamicSelectQueryBuilder Select(Func fn, params Func[] func); + + /// Add select columns. + /// Columns to add to object. + /// Builder instance. + IDynamicSelectQueryBuilder SelectColumn(params DynamicColumn[] columns); + + /// Add select columns. + /// Columns to add to object. + /// Column format consist of Column Name, Alias and + /// Aggregate function in this order separated by ':'. + /// Builder instance. + IDynamicSelectQueryBuilder SelectColumn(params string[] columns); + + #endregion Select + + #region GroupBy + + /// + /// Adds to the 'Group By' clause the contents obtained from from parsing the dynamic lambda expression given. + /// + /// The specification. + /// The specification. + /// This instance to permit chaining. + IDynamicSelectQueryBuilder GroupBy(Func fn, params Func[] func); + + /// Add select columns. + /// Columns to group by. + /// Builder instance. + IDynamicSelectQueryBuilder GroupByColumn(params DynamicColumn[] columns); + + /// Add select columns. + /// Columns to group by. + /// Column format consist of Column Name and + /// Alias in this order separated by ':'. + /// Builder instance. + IDynamicSelectQueryBuilder GroupByColumn(params string[] columns); + + #endregion GroupBy + + #region OrderBy + + /// + /// Adds to the 'Order By' clause the contents obtained from from parsing the dynamic lambda expression given. It + /// accepts a multipart column specification followed by an optional Ascending() or Descending() virtual methods + /// to specify the direction. If no virtual method is used, the default is ascending order. You can also use the + /// shorter versions Asc() and Desc(). + /// + /// The specification. + /// The specification. + /// This instance to permit chaining. + IDynamicSelectQueryBuilder OrderBy(Func fn, params Func[] func); + + /// Add select columns. + /// Columns to order by. + /// Builder instance. + IDynamicSelectQueryBuilder OrderByColumn(params DynamicColumn[] columns); + + /// Add select columns. + /// Columns to order by. + /// Column format consist of Column Name and + /// Alias in this order separated by ':'. + /// Builder instance. + IDynamicSelectQueryBuilder OrderByColumn(params string[] columns); + + #endregion OrderBy + + #region Top/Limit/Offset/Distinct + + /// Set top if database support it. + /// How many objects select. + /// Builder instance. + IDynamicSelectQueryBuilder Top(int? top); + + /// Set top if database support it. + /// How many objects select. + /// Builder instance. + IDynamicSelectQueryBuilder Limit(int? limit); + + /// Set top if database support it. + /// How many objects skip selecting. + /// Builder instance. + IDynamicSelectQueryBuilder Offset(int? offset); + + /// Set distinct mode. + /// Distinct mode. + /// Builder instance. + IDynamicSelectQueryBuilder Distinct(bool distinct = true); + + #endregion Top/Limit/Offset/Distinct + } + + /// Dynamic update query builder interface. + /// This interface it publically available. Implementation should be hidden. + public interface IDynamicUpdateQueryBuilder : IDynamicQueryBuilder + { + /// Execute this builder. + /// Result of an execution.. + int Execute(); + + #region Update + + /// Add update value or where condition using schema. + /// Update or where column name. + /// Column value. + /// Builder instance. + IDynamicUpdateQueryBuilder Update(string column, object value); + + /// Add update values and where condition columns using schema. + /// Set values or conditions as properties and values of an object. + /// Builder instance. + IDynamicUpdateQueryBuilder Update(object conditions); + + #endregion Update + + #region Values + + /// + /// Specifies the columns to update using the dynamic lambda expressions given. Each expression correspond to one + /// column, and can: + /// - Resolve to a string, in this case a '=' must appear in the string. + /// - Resolve to a expression with the form: 'x => x.Column = Value'. + /// + /// The specifications. + /// This instance to permit chaining. + IDynamicUpdateQueryBuilder Set(params Func[] func); + + /// Add insert fields. + /// Insert column. + /// Insert value. + /// Builder instance. + IDynamicUpdateQueryBuilder Values(string column, object value); + + /// Add insert fields. + /// Set insert value as properties and values of an object. + /// Builder instance. + IDynamicUpdateQueryBuilder Values(object o); + + #endregion Values + + #region Where + + /// + /// Adds to the 'Where' clause the contents obtained from parsing the dynamic lambda expression given. The condition + /// is parsed to the appropriate syntax, where the specific customs virtual methods supported by the parser are used + /// as needed. + /// - If several Where() methods are chained their contents are, by default, concatenated with an 'AND' operator. + /// - The 'And()' and 'Or()' virtual method can be used to concatenate with an 'OR' or an 'AND' operator, as in: + /// 'Where( x => x.Or( condition ) )'. + /// + /// The specification. + /// This instance to permit chaining. + IDynamicUpdateQueryBuilder Where(Func func); + + /// Add where condition. + /// Condition column with operator and value. + /// Builder instance. + IDynamicUpdateQueryBuilder Where(DynamicColumn column); + + /// Add where condition. + /// Condition column. + /// Condition operator. + /// Condition value. + /// Builder instance. + IDynamicUpdateQueryBuilder Where(string column, DynamicColumn.CompareOperator op, object value); + + /// Add where condition. + /// Condition column. + /// Condition value. + /// Builder instance. + IDynamicUpdateQueryBuilder Where(string column, object value); + + /// Add where condition. + /// Set conditions as properties and values of an object. + /// If true use schema to determine key columns and ignore those which + /// aren't keys. + /// Builder instance. + IDynamicUpdateQueryBuilder Where(object conditions, bool schema = false); + + #endregion Where + } + + /// Interface describing parameter info. + public interface IParameter + { + /// Gets the parameter position in command. + /// Available after filling the command. + int Ordinal { get; } + + /// Gets the parameter temporary name. + string Name { get; } + + /// Gets or sets the parameter value. + object Value { get; set; } + + /// Gets or sets a value indicating whether name of temporary parameter is well known. + bool WellKnown { get; set; } + + /// Gets or sets a value indicating whether this is virtual. + bool Virtual { get; set; } + + /// Gets or sets the parameter schema information. + DynamicSchemaColumn? Schema { get; set; } + } + + /// Interface describing table information. + public interface ITableInfo + { + /// Gets table owner name. + string Owner { get; } + + /// Gets table name. + string Name { get; } + + /// Gets table alias. + string Alias { get; } + + /// Gets table schema. + Dictionary Schema { get; } + } + + namespace Extensions + { + internal static class DynamicModifyBuilderExtensions + { + internal static T Table(this T builder, Func func) where T : DynamicModifyBuilder + { + if (func == null) + throw new ArgumentNullException("Function cannot be null."); + + using (var parser = DynamicParser.Parse(func)) + { + var result = parser.Result; + + // If the expression result is string. + if (result is string) + return builder.Table((string)result); + else if (result is Type) + return builder.Table((Type)result); + else if (result is DynamicParser.Node) + { + // Or if it resolves to a dynamic node + var node = (DynamicParser.Node)result; + + string owner = null; + string main = null; + + while (true) + { + // Deny support for the AS() virtual method... + if (node is DynamicParser.Node.Method && ((DynamicParser.Node.Method)node).Name.ToUpper() == "AS") + throw new ArgumentException(string.Format("Alias is not supported on modification builders. (Parsing: {0})", result)); + + // Support for table specifications... + if (node is DynamicParser.Node.GetMember) + { + if (owner != null) + throw new ArgumentException(string.Format("Owner '{0}.{1}' is already set when parsing '{2}'.", owner, main, result)); + + if (main != null) + owner = ((DynamicParser.Node.GetMember)node).Name; + else + main = ((DynamicParser.Node.GetMember)node).Name; + + node = node.Host; + continue; + } + + // Support for generic sources... + if (node is DynamicParser.Node.Invoke) + { + if (owner == null && main == null) + { + var invoke = (DynamicParser.Node.Invoke)node; + + if (invoke.Arguments.Length == 1 && invoke.Arguments[0] is Type) + return builder.Table((Type)invoke.Arguments[0]); + else if (invoke.Arguments.Length == 1 && invoke.Arguments[0] is String) + return builder.Table((string)invoke.Arguments[0]); + else + throw new ArgumentException(string.Format("Invalid argument count or type when parsing '{2}'. Invoke supports only one argument of type Type or String", owner, main, result)); + } + else if (owner != null) + throw new ArgumentException(string.Format("Owner '{0}.{1}' is already set when parsing '{2}'.", owner, main, result)); + else if (main != null) + throw new ArgumentException(string.Format("Main '{0}' is already set when parsing '{1}'.", main, result)); + } + + if (!string.IsNullOrEmpty(main)) + return builder.Table(string.Format("{0}{1}", + string.IsNullOrEmpty(owner) ? string.Empty : string.Format("{0}.", owner), + main)); + } + } + + throw new ArgumentException(string.Format("Unable to set table parsing '{0}'", result)); + } + } + + internal static T Table(this T builder, string tableName, Dictionary schema = null) where T : DynamicModifyBuilder + { + var tuple = tableName.Validated("Table Name").SplitSomethingAndAlias(); + + if (!string.IsNullOrEmpty(tuple.Item2)) + throw new ArgumentException(string.Format("Can not use aliases in INSERT steatement. ({0})", tableName), "tableName"); + + var parts = tuple.Item1.Split('.'); + + if (parts.Length > 2) + throw new ArgumentException(string.Format("Table name can consist only from name or owner and name. ({0})", tableName), "tableName"); + + builder.Tables.Clear(); + builder.Tables.Add(new DynamicQueryBuilder.TableInfo(builder.Database, + builder.Database.StripName(parts.Last()).Validated("Table"), null, + parts.Length == 2 ? builder.Database.StripName(parts.First()).Validated("Owner", canbeNull: true) : null)); + + if (schema != null) + (builder.Tables[0] as DynamicQueryBuilder.TableInfo).Schema = schema; + + return builder; + } + + internal static T Table(this T builder, Type type) where T : DynamicQueryBuilder + { + if (type.IsAnonymous()) + throw new InvalidOperationException(string.Format("Cant assign anonymous type as a table ({0}).", type.FullName)); + + var mapper = DynamicMapperCache.GetMapper(type); + + if (mapper == null) + throw new InvalidOperationException("Cant assign unmapable type as a table."); + + if (builder is DynamicModifyBuilder) + { + builder.Tables.Clear(); + builder.Tables.Add(new DynamicQueryBuilder.TableInfo(builder.Database, type)); + } + else if (builder is DynamicSelectQueryBuilder) + (builder as DynamicSelectQueryBuilder).From(x => x(type)); + + return builder; + } + } + + internal static class DynamicWhereQueryExtensions + { + #region Where + + internal static T InternalWhere(this T builder, Func func) where T : DynamicQueryBuilder, DynamicQueryBuilder.IQueryWithWhere + { + return builder.InternalWhere(false, false, func); + } + + internal static T InternalWhere(this T builder, bool addBeginBrace, bool addEndBrace, Func func) where T : DynamicQueryBuilder, DynamicQueryBuilder.IQueryWithWhere + { + if (func == null) throw new ArgumentNullException("Array of functions cannot be null."); + + using (var parser = DynamicParser.Parse(func)) + { + string condition = null; + bool and = true; + + var result = parser.Result; + if (result is string) + { + condition = (string)result; + + if (condition.ToUpper().IndexOf("OR") == 0) + { + and = false; + condition = condition.Substring(3); + } + else if (condition.ToUpper().IndexOf("AND") == 0) + condition = condition.Substring(4); + } + else if (!(result is DynamicParser.Node) && !result.GetType().IsValueType) + return builder.InternalWhere(result); + else + { + // Intercepting the 'x => x.And()' and 'x => x.Or()' virtual methods... + if (result is DynamicParser.Node.Method && ((DynamicParser.Node.Method)result).Host is DynamicParser.Node.Argument) + { + var node = (DynamicParser.Node.Method)result; + var name = node.Name.ToUpper(); + if (name == "AND" || name == "OR") + { + object[] args = ((DynamicParser.Node.Method)node).Arguments; + if (args == null) throw new ArgumentNullException("arg", string.Format("{0} is not a parameterless method.", name)); + if (args.Length != 1) throw new ArgumentException(string.Format("{0} requires one and only one parameter: {1}.", name, args.Sketch())); + + and = name == "AND" ? true : false; + result = args[0]; + } + } + + // Just parsing the contents now... + condition = builder.Parse(result, pars: builder.Parameters).Validated("Where condition"); + } + + if (addBeginBrace) builder.OpenBracketsCount++; + if (addEndBrace) builder.OpenBracketsCount--; + + if (builder.WhereCondition == null) + builder.WhereCondition = string.Format("{0}{1}{2}", + addBeginBrace ? "(" : string.Empty, condition, addEndBrace ? ")" : string.Empty); + else + builder.WhereCondition = string.Format("{0} {1} {2}{3}{4}", builder.WhereCondition, and ? "AND" : "OR", + addBeginBrace ? "(" : string.Empty, condition, addEndBrace ? ")" : string.Empty); + } + + return builder; + } + + internal static T InternalWhere(this T builder, DynamicColumn column) where T : DynamicQueryBuilder, DynamicQueryBuilder.IQueryWithWhere + { + bool virt = builder.VirtualMode; + if (column.VirtualColumn.HasValue) + builder.VirtualMode = column.VirtualColumn.Value; + + // It's kind of uglu, but... well it works. + if (column.Or) + switch (column.Operator) + { + default: + case DynamicColumn.CompareOperator.Eq: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x.Or(x(builder.FixObjectName(column.ColumnName)) == column.Value)); break; + case DynamicColumn.CompareOperator.Not: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x.Or(x(builder.FixObjectName(column.ColumnName)) != column.Value)); break; + case DynamicColumn.CompareOperator.Like: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x.Or(x(builder.FixObjectName(column.ColumnName)).Like(column.Value))); break; + case DynamicColumn.CompareOperator.NotLike: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x.Or(x(builder.FixObjectName(column.ColumnName)).NotLike(column.Value))); break; + case DynamicColumn.CompareOperator.In: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x.Or(x(builder.FixObjectName(column.ColumnName)).In(column.Value))); break; + case DynamicColumn.CompareOperator.Lt: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x.Or(x(builder.FixObjectName(column.ColumnName)) < column.Value)); break; + case DynamicColumn.CompareOperator.Lte: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x.Or(x(builder.FixObjectName(column.ColumnName)) <= column.Value)); break; + case DynamicColumn.CompareOperator.Gt: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x.Or(x(builder.FixObjectName(column.ColumnName)) > column.Value)); break; + case DynamicColumn.CompareOperator.Gte: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x.Or(x(builder.FixObjectName(column.ColumnName)) >= column.Value)); break; + case DynamicColumn.CompareOperator.Between: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x.Or(x(builder.FixObjectName(column.ColumnName)).Between(column.Value))); break; + } + else + switch (column.Operator) + { + default: + case DynamicColumn.CompareOperator.Eq: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x(builder.FixObjectName(column.ColumnName)) == column.Value); break; + case DynamicColumn.CompareOperator.Not: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x(builder.FixObjectName(column.ColumnName)) != column.Value); break; + case DynamicColumn.CompareOperator.Like: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x(builder.FixObjectName(column.ColumnName)).Like(column.Value)); break; + case DynamicColumn.CompareOperator.NotLike: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x(builder.FixObjectName(column.ColumnName)).NotLike(column.Value)); break; + case DynamicColumn.CompareOperator.In: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x(builder.FixObjectName(column.ColumnName)).In(column.Value)); break; + case DynamicColumn.CompareOperator.Lt: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x(builder.FixObjectName(column.ColumnName)) < column.Value); break; + case DynamicColumn.CompareOperator.Lte: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x(builder.FixObjectName(column.ColumnName)) <= column.Value); break; + case DynamicColumn.CompareOperator.Gt: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x(builder.FixObjectName(column.ColumnName)) > column.Value); break; + case DynamicColumn.CompareOperator.Gte: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x(builder.FixObjectName(column.ColumnName)) >= column.Value); break; + case DynamicColumn.CompareOperator.Between: builder.InternalWhere(column.BeginBlock, column.EndBlock, x => x(builder.FixObjectName(column.ColumnName)).Between(column.Value)); break; + } + + builder.VirtualMode = virt; + + return builder; + } + + internal static T InternalWhere(this T builder, string column, DynamicColumn.CompareOperator op, object value) where T : DynamicQueryBuilder, DynamicQueryBuilder.IQueryWithWhere + { + if (value is DynamicColumn) + { + var v = (DynamicColumn)value; + + if (string.IsNullOrEmpty(v.ColumnName)) + v.ColumnName = column; + + return builder.InternalWhere(v); + } + else if (value is IEnumerable) + { + foreach (DynamicColumn v in (IEnumerable)value) + builder.InternalWhere(v); + + return builder; + } + + return builder.InternalWhere(new DynamicColumn + { + ColumnName = column, + Operator = op, + Value = value + }); + } + + internal static T InternalWhere(this T builder, string column, object value) where T : DynamicQueryBuilder, DynamicQueryBuilder.IQueryWithWhere + { + return builder.InternalWhere(column, DynamicColumn.CompareOperator.Eq, value); + } + + internal static T InternalWhere(this T builder, object conditions, bool schema = false) where T : DynamicQueryBuilder, DynamicQueryBuilder.IQueryWithWhere + { + if (conditions is DynamicColumn) + return builder.InternalWhere((DynamicColumn)conditions); + else if (conditions is IEnumerable) + { + foreach (DynamicColumn v in (IEnumerable)conditions) + builder.InternalWhere(v); + + return builder; + } + + var dict = conditions.ToDictionary(); + var mapper = DynamicMapperCache.GetMapper(conditions.GetType()); + var table = dict.TryGetValue("_table").NullOr(x => x.ToString(), string.Empty); + + foreach (var condition in dict) + { + if (mapper.Ignored.Contains(condition.Key) || condition.Key == "_table") + continue; + + string colName = mapper != null ? mapper.PropertyMap.TryGetValue(condition.Key) ?? condition.Key : condition.Key; + + DynamicSchemaColumn? col = null; + + // This should be used on typed queries or update/delete steatements, which usualy operate on a single table. + if (schema) + { + col = builder.GetColumnFromSchema(colName, mapper, table); + + if ((!col.HasValue || !col.Value.IsKey) && + (mapper == null || mapper.ColumnsMap.TryGetValue(colName).NullOr(m => m.Ignore || m.Column.NullOr(c => !c.IsKey, true), true))) + continue; + + colName = col.HasValue ? col.Value.Name : colName; + } + + if (!string.IsNullOrEmpty(table)) + builder.InternalWhere(x => x(builder.FixObjectName(string.Format("{0}.{1}", table, colName))) == condition.Value); + else + builder.InternalWhere(x => x(builder.FixObjectName(colName)) == condition.Value); + } + + return builder; + } + + #endregion Where + } + } + + namespace Implementation + { + /// Implementation of dynamic delete query builder. + internal class DynamicDeleteQueryBuilder : DynamicModifyBuilder, IDynamicDeleteQueryBuilder, DynamicQueryBuilder.IQueryWithWhere + { + /// + /// Initializes a new instance of the class. + /// + /// The database. + internal DynamicDeleteQueryBuilder(DynamicDatabase db) + : base(db) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The database. + /// Name of the table. + public DynamicDeleteQueryBuilder(DynamicDatabase db, string tableName) + : base(db, tableName) + { + } + + /// Generates the text this command will execute against the underlying database. + /// The text to execute against the underlying database. + /// This method must be override by derived classes. + public override string CommandText() + { + var info = Tables.Single(); + return string.Format("DELETE FROM {0}{1}{2}{3}", + string.IsNullOrEmpty(info.Owner) ? string.Empty : string.Format("{0}.", Database.DecorateName(info.Owner)), + Database.DecorateName(info.Name), + string.IsNullOrEmpty(WhereCondition) ? string.Empty : " WHERE ", + WhereCondition); + } + + #region Where + + /// + /// Adds to the 'Where' clause the contents obtained from parsing the dynamic lambda expression given. The condition + /// is parsed to the appropriate syntax, where the specific customs virtual methods supported by the parser are used + /// as needed. + /// - If several Where() methods are chained their contents are, by default, concatenated with an 'AND' operator. + /// - The 'And()' and 'Or()' virtual method can be used to concatenate with an 'OR' or an 'AND' operator, as in: + /// 'Where( x => x.Or( condition ) )'. + /// + /// The specification. + /// This instance to permit chaining. + public virtual IDynamicDeleteQueryBuilder Where(Func func) + { + return this.InternalWhere(func); + } + + /// Add where condition. + /// Condition column with operator and value. + /// Builder instance. + public virtual IDynamicDeleteQueryBuilder Where(DynamicColumn column) + { + return this.InternalWhere(column); + } + + /// Add where condition. + /// Condition column. + /// Condition operator. + /// Condition value. + /// Builder instance. + public virtual IDynamicDeleteQueryBuilder Where(string column, DynamicColumn.CompareOperator op, object value) + { + return this.InternalWhere(column, op, value); + } + + /// Add where condition. + /// Condition column. + /// Condition value. + /// Builder instance. + public virtual IDynamicDeleteQueryBuilder Where(string column, object value) + { + return this.InternalWhere(column, value); + } + + /// Add where condition. + /// Set conditions as properties and values of an object. + /// If true use schema to determine key columns and ignore those which + /// aren't keys. + /// Builder instance. + public virtual IDynamicDeleteQueryBuilder Where(object conditions, bool schema = false) + { + return this.InternalWhere(conditions, schema); + } + + #endregion Where + } + + /// Implementation of dynamic insert query builder. + internal class DynamicInsertQueryBuilder : DynamicModifyBuilder, IDynamicInsertQueryBuilder + { + private string _columns; + private string _values; + + /// + /// Initializes a new instance of the class. + /// + /// The database. + internal DynamicInsertQueryBuilder(DynamicDatabase db) + : base(db) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The database. + /// Name of the table. + public DynamicInsertQueryBuilder(DynamicDatabase db, string tableName) + : base(db, tableName) + { + } + + /// Generates the text this command will execute against the underlying database. + /// The text to execute against the underlying database. + /// This method must be override by derived classes. + public override string CommandText() + { + var info = Tables.Single(); + return string.Format("INSERT INTO {0}{1} ({2}) VALUES ({3})", + string.IsNullOrEmpty(info.Owner) ? string.Empty : string.Format("{0}.", Database.DecorateName(info.Owner)), + Database.DecorateName(info.Name), _columns, _values); + } + + #region Insert + + /// + /// Specifies the columns to insert using the dynamic lambda expressions given. Each expression correspond to one + /// column, and can: + /// - Resolve to a string, in this case a '=' must appear in the string. + /// - Resolve to a expression with the form: 'x => x.Column = Value'. + /// + /// The specifications. + /// The specifications. + /// This instance to permit chaining. + public virtual IDynamicInsertQueryBuilder Values(Func fn, params Func[] func) + { + if (fn == null) + throw new ArgumentNullException("Array of specifications cannot be null."); + + int index = InsertFunc(-1, fn); + + if (func != null) + foreach (var f in func) + index = InsertFunc(index, f); + + return this; + } + + private int InsertFunc(int index, Func f) + { + index++; + + if (f == null) + throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); + + using (var parser = DynamicParser.Parse(f)) + { + var result = parser.Result; + if (result == null) + throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); + + string main = null; + string value = null; + string str = null; + + // When 'x => x.Table.Column = value' or 'x => x.Column = value'... + if (result is DynamicParser.Node.SetMember) + { + var node = (DynamicParser.Node.SetMember)result; + + DynamicSchemaColumn? col = GetColumnFromSchema(node.Name); + main = Database.DecorateName(node.Name); + value = Parse(node.Value, pars: Parameters, nulls: true, columnSchema: col); + + _columns = _columns == null ? main : string.Format("{0}, {1}", _columns, main); + _values = _values == null ? value : string.Format("{0}, {1}", _values, value); + return index; + } + else if (!(result is DynamicParser.Node) && !result.GetType().IsValueType) + { + Insert(result); + return index; + } + + // Other specifications are considered invalid... + var err = string.Format("Specification '{0}' is invalid.", result); + str = Parse(result); + if (str.Contains("=")) err += " May have you used a '==' instead of a '=' operator?"; + throw new ArgumentException(err); + } + } + + /// Add insert fields. + /// Insert column. + /// Insert value. + /// Builder instance. + public virtual IDynamicInsertQueryBuilder Insert(string column, object value) + { + if (value is DynamicColumn) + { + var v = (DynamicColumn)value; + + if (string.IsNullOrEmpty(v.ColumnName)) + v.ColumnName = column; + + return Insert(v); + } + + return Insert(new DynamicColumn + { + ColumnName = column, + Value = value, + }); + } + + /// Add insert fields. + /// Set insert value as properties and values of an object. + /// Builder instance. + public virtual IDynamicInsertQueryBuilder Insert(object o) + { + if (o is DynamicColumn) + { + var column = (DynamicColumn)o; + DynamicSchemaColumn? col = column.Schema ?? GetColumnFromSchema(column.ColumnName); + + string main = FixObjectName(column.ColumnName, onlyColumn: true); + string value = Parse(column.Value, pars: Parameters, nulls: true, columnSchema: col); + + _columns = _columns == null ? main : string.Format("{0}, {1}", _columns, main); + _values = _values == null ? value : string.Format("{0}, {1}", _values, value); + + return this; + } + + var dict = o.ToDictionary(); + var mapper = DynamicMapperCache.GetMapper(o.GetType()); + + if (mapper != null) + { + foreach (var con in dict) + if (!mapper.Ignored.Contains(con.Key)) + Insert(mapper.PropertyMap.TryGetValue(con.Key) ?? con.Key, con.Value); + } + else + foreach (var con in dict) + Insert(con.Key, con.Value); + + return this; + } + + #endregion Insert + } + + /// Base query builder for insert/update/delete statements. + internal abstract class DynamicModifyBuilder : DynamicQueryBuilder + { + /// + /// Initializes a new instance of the class. + /// + /// The database. + public DynamicModifyBuilder(DynamicDatabase db) + : base(db) + { + VirtualMode = false; + } + + /// + /// Initializes a new instance of the class. + /// + /// The database. + /// Name of the table. + public DynamicModifyBuilder(DynamicDatabase db, string tableName) + : this(db) + { + VirtualMode = false; + this.Table(tableName); + } + + /// Execute this builder. + /// Result of an execution.. + public virtual int Execute() + { + using (var con = Database.Open()) + using (var cmd = con.CreateCommand()) + { + return cmd + .SetCommand(this) + .ExecuteNonQuery(); + } + } + } + + /// Implementation of dynamic query builder base interface. + internal abstract class DynamicQueryBuilder : IDynamicQueryBuilder + { + /// Empty interface to allow where query builder implementation use universal approach. + internal interface IQueryWithWhere + { + /// Gets or sets the where condition. + string WhereCondition { get; set; } + + /// Gets or sets the amount of not closed brackets in where statement. + int OpenBracketsCount { get; set; } + } + + private DynamicQueryBuilder _parent = null; + + #region TableInfo + + /// Table information. + internal class TableInfo : ITableInfo + { + /// + /// Initializes a new instance of the class. + /// + internal TableInfo() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The database. + /// The name of table. + /// The table alias. + /// The table owner. + public TableInfo(DynamicDatabase db, string name, string alias = null, string owner = null) + { + Name = name; + Alias = alias; + Owner = owner; + + if (!name.ContainsAny(StringExtensions.InvalidMemberChars)) + Schema = db.GetSchema(name, owner: owner); + } + + /// + /// Initializes a new instance of the class. + /// + /// The database. + /// The type which can be mapped to database. + /// The table alias. + /// The table owner. + public TableInfo(DynamicDatabase db, Type type, string alias = null, string owner = null) + { + var mapper = DynamicMapperCache.GetMapper(type); + + Name = mapper.Table == null || string.IsNullOrEmpty(mapper.Table.Name) ? + mapper.Type.Name : mapper.Table.Name; + + Owner = (mapper.Table != null) ? mapper.Table.Owner : owner; + Alias = alias; + + Schema = db.GetSchema(type); + } + + /// Gets or sets table owner name. + public string Owner { get; internal set; } + + /// Gets or sets table name. + public string Name { get; internal set; } + + /// Gets or sets table alias. + public string Alias { get; internal set; } + + /// Gets or sets table schema. + public Dictionary Schema { get; internal set; } + } + + /// Generic based table information. + /// Type of class that is represented in database. + internal class TableInfo : TableInfo + { + /// + /// Initializes a new instance of the class. + /// + /// The database. + /// The table alias. + /// The table owner. + public TableInfo(DynamicDatabase db, string alias = null, string owner = null) + : base(db, typeof(T), alias, owner) + { + } + } + + #endregion TableInfo + + #region Parameter + + /// Interface describing parameter info. + internal class Parameter : IParameter + { + /// Gets or sets the parameter position in command. + /// Available after filling the command. + public int Ordinal { get; internal set; } + + /// Gets or sets the parameter temporary name. + public string Name { get; internal set; } + + /// Gets or sets the parameter value. + public object Value { get; set; } + + /// Gets or sets a value indicating whether name of temporary parameter is well known. + public bool WellKnown { get; set; } + + /// Gets or sets a value indicating whether this is virtual. + public bool Virtual { get; set; } + + /// Gets or sets the parameter schema information. + public DynamicSchemaColumn? Schema { get; set; } + } + + #endregion Parameter + + #region Constructor + + /// + /// Initializes a new instance of the class. + /// + /// The database. + public DynamicQueryBuilder(DynamicDatabase db) + { + VirtualMode = false; + Tables = new List(); + Parameters = new Dictionary(); + + WhereCondition = null; + OpenBracketsCount = 0; + + Database = db; + SupportSchema = (db.Options & DynamicDatabaseOptions.SupportSchema) == DynamicDatabaseOptions.SupportSchema; + } + + /// Initializes a new instance of the class. + /// The database. + /// The parent query. + internal DynamicQueryBuilder(DynamicDatabase db, DynamicQueryBuilder parent) + : this(db) + { + _parent = parent; + } + + #endregion Constructor + + #region IQueryWithWhere + + /// Gets or sets the where condition. + public string WhereCondition { get; set; } + + /// Gets or sets the amount of not closed brackets in where statement. + public int OpenBracketsCount { get; set; } + + #endregion IQueryWithWhere + + #region IDynamicQueryBuilder + + /// Gets instance. + public DynamicDatabase Database { get; private set; } + + /// Gets the tables used in this builder. + public IList Tables { get; private set; } + + /// Gets the tables used in this builder. + public IDictionary Parameters { get; private set; } + + /// Gets or sets a value indicating whether add virtual parameters. + public bool VirtualMode { get; set; } + + /// Gets or sets the on create temporary parameter action. + /// This is exposed to allow setting schema of column. + public Action OnCreateTemporaryParameter { get; set; } + + /// Gets or sets the on create real parameter action. + /// This is exposed to allow modification of parameter. + public Action OnCreateParameter { get; set; } + + /// Gets a value indicating whether database supports standard schema. + public bool SupportSchema { get; private set; } + + /// + /// Generates the text this command will execute against the underlying database. + /// + /// The text to execute against the underlying database. + /// This method must be override by derived classes. + public abstract string CommandText(); + + /// Fill command with query. + /// Command to fill. + /// Filled instance of . + public virtual IDbCommand FillCommand(IDbCommand command) + { + // End not ended where statement + if (this is IQueryWithWhere) + { + while (OpenBracketsCount > 0) + { + WhereCondition += ")"; + OpenBracketsCount--; + } + } + + return command.SetCommand(CommandText() + .FillStringWithVariables(s => + { + return Parameters.TryGetValue(s).NullOr(p => + { + IDbDataParameter param = (IDbDataParameter)command + .AddParameter(this, p.Schema, p.Value) + .Parameters[command.Parameters.Count - 1]; + + (p as Parameter).Ordinal = command.Parameters.Count - 1; + + if (OnCreateParameter != null) + OnCreateParameter(p, param); + + return param.ParameterName; + }, s); + })); + } + + #endregion IDynamicQueryBuilder + + #region Parser + + /// Parses the arbitrary object given and translates it into a string with the appropriate + /// syntax for the database this parser is specific to. + /// The object to parse and translate. It can be any arbitrary object, including null values (if + /// permitted) and dynamic lambda expressions. + /// If not null, the parameters' list where to store the parameters extracted by the parsing. + /// If true, literal (raw) string are allowed. If false and the node is a literal then, as a + /// security measure, an exception is thrown. + /// True to accept null values and translate them into the appropriate syntax accepted by the + /// database. If false and the value is null, then an exception is thrown. + /// If set to true decorate element. + /// If set parse argument as alias. This is workaround for AS method. + /// This parameter is used to determine type of parameter used in query. + /// A string containing the result of the parsing, along with the parameters extracted in the + /// instance if such is given. + /// Null nodes are not accepted. + internal virtual string Parse(object node, IDictionary pars = null, bool rawstr = false, bool nulls = false, bool decorate = true, bool isMultiPart = true, DynamicSchemaColumn? columnSchema = null) + { + // Null nodes are accepted or not depending upon the "nulls" flag... + if (node == null) + { + if (!nulls) + throw new ArgumentNullException("node", "Null nodes are not accepted."); + + return Dispatch(node, pars, decorate, columnSchema: columnSchema); + } + + // Nodes that are strings are parametrized or not depending the "rawstr" flag... + if (node is string) + { + if (rawstr) return (string)node; + else return Dispatch(node, pars, decorate, columnSchema: columnSchema); + } + + // If node is a delegate, parse it to create the logical tree... + if (node is Delegate) + { + node = DynamicParser.Parse((Delegate)node).Result; + return Parse(node, pars, rawstr, decorate: decorate, columnSchema: columnSchema); // Intercept containers as in (x => "string") + } + + return Dispatch(node, pars, decorate, isMultiPart, columnSchema); + } + + private string Dispatch(object node, IDictionary pars = null, bool decorate = true, bool isMultiPart = true, DynamicSchemaColumn? columnSchema = null) + { + if (node != null) + { + if (node is DynamicQueryBuilder) return ParseCommand((DynamicQueryBuilder)node, pars); + else if (node is DynamicParser.Node.Argument) return ParseArgument((DynamicParser.Node.Argument)node, isMultiPart); + else if (node is DynamicParser.Node.GetMember) return ParseGetMember((DynamicParser.Node.GetMember)node, pars, decorate, isMultiPart, columnSchema); + else if (node is DynamicParser.Node.SetMember) return ParseSetMember((DynamicParser.Node.SetMember)node, pars, decorate, isMultiPart, columnSchema); + else if (node is DynamicParser.Node.Unary) return ParseUnary((DynamicParser.Node.Unary)node, pars); + else if (node is DynamicParser.Node.Binary) return ParseBinary((DynamicParser.Node.Binary)node, pars); + else if (node is DynamicParser.Node.Method) return ParseMethod((DynamicParser.Node.Method)node, pars); + else if (node is DynamicParser.Node.Invoke) return ParseInvoke((DynamicParser.Node.Invoke)node, pars); + else if (node is DynamicParser.Node.Convert) return ParseConvert((DynamicParser.Node.Convert)node, pars); + } + + // All other cases are considered constant parameters... + return ParseConstant(node, pars, columnSchema); + } + + internal virtual string ParseCommand(DynamicQueryBuilder node, IDictionary pars = null) + { + // Getting the command's text... + string str = node.CommandText(); // Avoiding spurious "OUTPUT XXX" statements + + // If there are parameters to transform, but cannot store them, it is an error + if (node.Parameters.Count != 0 && pars == null) + throw new InvalidOperationException(string.Format("The parameters in this command '{0}' cannot be added to a null collection.", node.Parameters)); + + // Copy parameters to new comand + foreach (var parameter in node.Parameters) + pars.Add(parameter.Key, parameter.Value); + + return string.Format("({0})", str); + } + + protected virtual string ParseArgument(DynamicParser.Node.Argument node, bool isMultiPart = true, bool isOwner = false) + { + if (!string.IsNullOrEmpty(node.Name) && (isOwner || (isMultiPart && IsTableAlias(node.Name)))) + return node.Name; + + return null; + } + + protected virtual string ParseGetMember(DynamicParser.Node.GetMember node, IDictionary pars = null, bool decorate = true, bool isMultiPart = true, DynamicSchemaColumn? columnSchema = null) + { + if (node.Host is DynamicParser.Node.Argument && IsTableAlias(node.Name)) + { + decorate = false; + isMultiPart = false; + } + + // This hack allows to use argument as alias, but when it is not nesesary use other column. + // Let say we hace a table Users with alias usr, and we join to table with alias ua which also has a column Users + // This allow use of usr => usr.ua.Users to result in ua."Users" instead of "Users" or usr."ua"."Users", se tests for examples. + string parent = null; + if (node.Host != null) + { + if (isMultiPart && node.Host is DynamicParser.Node.GetMember && IsTable(node.Host.Name, null)) + { + if (node.Host.Host != null && node.Host.Host is DynamicParser.Node.GetMember && IsTable(node.Host.Name, node.Host.Host.Name)) + parent = string.Format("{0}.{1}", Parse(node.Host.Host, pars, isMultiPart: false), Parse(node.Host, pars, isMultiPart: false)); + else + parent = Parse(node.Host, pars, isMultiPart: false); + } + else if (isMultiPart) + parent = Parse(node.Host, pars, isMultiPart: isMultiPart); + } + + ////string parent = node.Host == null || !isMultiPart ? null : Parse(node.Host, pars, isMultiPart: !IsTable(node.Name, node.Host.Name)); + string name = parent == null ? + decorate ? Database.DecorateName(node.Name) : node.Name : + string.Format("{0}.{1}", parent, decorate ? Database.DecorateName(node.Name) : node.Name); + + columnSchema = GetColumnFromSchema(name); + + return name; + } + + protected virtual string ParseSetMember(DynamicParser.Node.SetMember node, IDictionary pars = null, bool decorate = true, bool isMultiPart = true, DynamicSchemaColumn? columnSchema = null) + { + if (node.Host is DynamicParser.Node.Argument && IsTableAlias(node.Name)) + { + decorate = false; + isMultiPart = false; + } + + string parent = null; + if (node.Host != null) + { + if (isMultiPart && node.Host is DynamicParser.Node.GetMember && IsTable(node.Host.Name, null)) + { + if (node.Host.Host != null && node.Host.Host is DynamicParser.Node.GetMember && IsTable(node.Name, node.Host.Name)) + parent = string.Format("{0}.{1}", Parse(node.Host.Host, pars, isMultiPart: false), Parse(node.Host, pars, isMultiPart: false)); + else + parent = Parse(node.Host, pars, isMultiPart: false); + } + else if (isMultiPart) + parent = Parse(node.Host, pars, isMultiPart: isMultiPart); + } + + ////string parent = node.Host == null || !isMultiPart ? null : Parse(node.Host, pars, isMultiPart: !IsTable(node.Name, node.Host.Name)); + string name = parent == null ? + decorate ? Database.DecorateName(node.Name) : node.Name : + string.Format("{0}.{1}", parent, decorate ? Database.DecorateName(node.Name) : node.Name); + + columnSchema = GetColumnFromSchema(name); + + string value = Parse(node.Value, pars, nulls: true, columnSchema: columnSchema); + return string.Format("{0} = ({1})", name, value); + } + + protected virtual string ParseUnary(DynamicParser.Node.Unary node, IDictionary pars = null) + { + switch (node.Operation) + { + // Artifacts from the DynamicParser class that are not usefull here... + case ExpressionType.IsFalse: + case ExpressionType.IsTrue: return Parse(node.Target, pars); + + // Unary supported operations... + case ExpressionType.Not: return string.Format("(NOT {0})", Parse(node.Target, pars)); + case ExpressionType.Negate: return string.Format("!({0})", Parse(node.Target, pars)); + } + + throw new ArgumentException("Not supported unary operation: " + node); + } + + protected virtual string ParseBinary(DynamicParser.Node.Binary node, IDictionary pars = null) + { + string op = string.Empty; + + switch (node.Operation) + { + // Arithmetic binary operations... + case ExpressionType.Add: op = "+"; break; + case ExpressionType.Subtract: op = "-"; break; + case ExpressionType.Multiply: op = "*"; break; + case ExpressionType.Divide: op = "/"; break; + case ExpressionType.Modulo: op = "%"; break; + case ExpressionType.Power: op = "^"; break; + + case ExpressionType.And: op = "AND"; break; + case ExpressionType.Or: op = "OR"; break; + + // Logical comparisons... + case ExpressionType.GreaterThan: op = ">"; break; + case ExpressionType.GreaterThanOrEqual: op = ">="; break; + case ExpressionType.LessThan: op = "<"; break; + case ExpressionType.LessThanOrEqual: op = "<="; break; + + // Comparisons against 'NULL' require the 'IS' or 'IS NOT' operator instead the numeric ones... + case ExpressionType.Equal: op = node.Right == null && !VirtualMode ? "IS" : "="; break; + case ExpressionType.NotEqual: op = node.Right == null && !VirtualMode ? "IS NOT" : "<>"; break; + + default: throw new ArgumentException("Not supported operator: '" + node.Operation); + } + + DynamicSchemaColumn? columnSchema = null; + string left = Parse(node.Left, pars, columnSchema: columnSchema); // Not nulls: left is assumed to be an object + string right = Parse(node.Right, pars, nulls: true, columnSchema: columnSchema); + return string.Format("({0} {1} {2})", left, op, right); + } + + protected virtual string ParseMethod(DynamicParser.Node.Method node, IDictionary pars = null) + { + string method = node.Name.ToUpper(); + string parent = node.Host == null ? null : Parse(node.Host, pars: pars); + string item = null; + + // Root-level methods... + if (node.Host == null) + { + switch (method) + { + case "NOT": + if (node.Arguments == null || node.Arguments.Length != 1) throw new ArgumentNullException("NOT method expects one argument: " + node.Arguments.Sketch()); + item = Parse(node.Arguments[0], pars: pars); + return string.Format("(NOT {0})", item); + } + } + + // Column-level methods... + if (node.Host != null) + { + switch (method) + { + case "BETWEEN": + { + if (node.Arguments == null || node.Arguments.Length == 0) + throw new ArgumentException("BETWEEN method expects at least one argument: " + node.Arguments.Sketch()); + + if (node.Arguments.Length > 2) + throw new ArgumentException("BETWEEN method expects at most two arguments: " + node.Arguments.Sketch()); + + var arguments = node.Arguments; + + if (arguments.Length == 1 && (arguments[0] is IEnumerable || arguments[0] is Array) && !(arguments[0] is byte[])) + { + var vals = arguments[0] as IEnumerable; + + if (vals == null && arguments[0] is Array) + vals = ((Array)arguments[0]).Cast() as IEnumerable; + + if (vals != null) + arguments = vals.ToArray(); + else + throw new ArgumentException("BETWEEN method expects single argument to be enumerable of exactly two elements: " + node.Arguments.Sketch()); + } + + return string.Format("{0} BETWEEN {1} AND {2}", parent, Parse(arguments[0], pars: pars), Parse(arguments[1], pars: pars)); + } + + case "IN": + { + if (node.Arguments == null || node.Arguments.Length == 0) + throw new ArgumentException("IN method expects at least one argument: " + node.Arguments.Sketch()); + + bool firstParam = true; + StringBuilder sbin = new StringBuilder(); + foreach (var arg in node.Arguments) + { + if (!firstParam) + sbin.Append(", "); + + if ((arg is IEnumerable || arg is Array) && !(arg is byte[])) + { + var vals = arg as IEnumerable; + + if (vals == null && arg is Array) + vals = ((Array)arg).Cast() as IEnumerable; + + if (vals != null) + foreach (var val in vals) + { + if (!firstParam) + sbin.Append(", "); + else + firstParam = false; + + sbin.Append(Parse(val, pars: pars)); + } + else + sbin.Append(Parse(arg, pars: pars)); + } + else + sbin.Append(Parse(arg, pars: pars)); + + firstParam = false; + } + + return string.Format("{0} IN({1})", parent, sbin.ToString()); + } + + case "LIKE": + if (node.Arguments == null || node.Arguments.Length != 1) + throw new ArgumentException("LIKE method expects one argument: " + node.Arguments.Sketch()); + + return string.Format("{0} LIKE {1}", parent, Parse(node.Arguments[0], pars: pars)); + + case "NOTLIKE": + if (node.Arguments == null || node.Arguments.Length != 1) + throw new ArgumentException("NOT LIKE method expects one argument: " + node.Arguments.Sketch()); + + return string.Format("{0} NOT LIKE {1}", parent, Parse(node.Arguments[0], pars: pars)); + + case "AS": + if (node.Arguments == null || node.Arguments.Length != 1) + throw new ArgumentException("AS method expects one argument: " + node.Arguments.Sketch()); + + item = Parse(node.Arguments[0], pars: null, rawstr: true, isMultiPart: false); // pars=null to avoid to parameterize aliases + item = item.Validated("Alias"); // Intercepting null and empty aliases + return string.Format("{0} AS {1}", parent, item); + + case "COUNT": + if (node.Arguments != null && node.Arguments.Length > 1) + throw new ArgumentException("COUNT method expects one or none argument: " + node.Arguments.Sketch()); + + if (node.Arguments == null || node.Arguments.Length == 0) + return "COUNT(*)"; + + return string.Format("COUNT({0})", Parse(node.Arguments[0], pars: Parameters, nulls: true)); + } + } + + // Default case: parsing the method's name along with its arguments... + method = parent == null ? node.Name : string.Format("{0}.{1}", parent, node.Name); + StringBuilder sb = new StringBuilder(); + sb.AppendFormat("{0}(", method); + + if (node.Arguments != null && node.Arguments.Length != 0) + { + bool first = true; + + foreach (object argument in node.Arguments) + { + if (!first) + sb.Append(", "); + else + first = false; + + sb.Append(Parse(argument, pars, nulls: true)); // We don't accept raw strings here!!! + } + } + + sb.Append(")"); + return sb.ToString(); + } + + protected virtual string ParseInvoke(DynamicParser.Node.Invoke node, IDictionary pars = null) + { + // This is used as an especial syntax to merely concatenate its arguments. It is used as a way to extend the supported syntax without the need of treating all the possible cases... + if (node.Arguments == null || node.Arguments.Length == 0) + return string.Empty; + + StringBuilder sb = new StringBuilder(); + foreach (object arg in node.Arguments) + { + if (arg is string) + sb.Append((string)arg); + else + sb.Append(Parse(arg, pars, rawstr: true, nulls: true)); + } + + return sb.ToString(); + } + + protected virtual string ParseConvert(DynamicParser.Node.Convert node, IDictionary pars = null) + { + // The cast mechanism is left for the specific database implementation, that should override this method + // as needed... + string r = Parse(node.Target, pars); + return r; + } + + protected virtual string ParseConstant(object node, IDictionary pars = null, DynamicSchemaColumn? columnSchema = null) + { + if (node == null && !VirtualMode) + return ParseNull(); + + if (pars != null) + { + bool wellKnownName = VirtualMode && node is String && ((String)node).StartsWith("[$") && ((String)node).EndsWith("]") && ((String)node).Length > 4; + + // If we have a list of parameters to store it, let's parametrize it + var par = new Parameter() + { + Name = wellKnownName ? ((String)node).Substring(2, ((String)node).Length - 3) : Guid.NewGuid().ToString(), + Value = wellKnownName ? null : node, + WellKnown = wellKnownName, + Virtual = VirtualMode, + Schema = columnSchema, + }; + + // If we are adding parameter we inform external sources about this. + if (OnCreateTemporaryParameter != null) + OnCreateTemporaryParameter(par); + + pars.Add(par.Name, par); + + return string.Format("[${0}]", par.Name); + } + + return node.ToString(); // Last resort case + } + + protected virtual string ParseNull() + { + return "NULL"; // Override if needed + } + + #endregion Parser + + #region Helpers + + internal bool IsTableAlias(string name) + { + DynamicQueryBuilder builder = this; + + while (builder != null) + { + if (builder.Tables.Any(t => t.Alias == name)) + return true; + + builder = builder._parent; + } + + return false; + } + + internal bool IsTable(string name, string owner) + { + DynamicQueryBuilder builder = this; + + while (builder != null) + { + if ((string.IsNullOrEmpty(owner) && builder.Tables.Any(t => t.Name.ToLower() == name.ToLower())) || + (!string.IsNullOrEmpty(owner) && builder.Tables.Any(t => t.Name.ToLower() == name.ToLower() && + !string.IsNullOrEmpty(t.Owner) && t.Owner.ToLower() == owner.ToLower()))) + return true; + + builder = builder._parent; + } + + return false; + } + + internal string FixObjectName(string main, bool onlyColumn = false) + { + if (main.IndexOf("(") > 0 && main.IndexOf(")") > 0) + return main.FillStringWithVariables(f => string.Format("({0})", FixObjectNamePrivate(f, onlyColumn)), "(", ")"); + else + return FixObjectNamePrivate(main, onlyColumn); + } + + private string FixObjectNamePrivate(string f, bool onlyColumn = false) + { + var objects = f.Split('.') + .Select(x => Database.StripName(x)); + + if (onlyColumn || objects.Count() == 1) + f = Database.DecorateName(objects.Last()); + else if (!IsTableAlias(objects.First())) + f = string.Join(".", objects.Select(o => Database.DecorateName(o))); + else + f = string.Format("{0}.{1}", objects.First(), string.Join(".", objects.Skip(1).Select(o => Database.DecorateName(o)))); + + return f; + } + + internal DynamicSchemaColumn? GetColumnFromSchema(string colName, DynamicTypeMap mapper = null, string table = null) + { + // This is tricky and will not always work unfortunetly. + if (colName.ContainsAny(StringExtensions.InvalidMultipartMemberChars)) + return null; + + // First we need to get real column name and it's owner if exist. + var parts = colName.Split('.') + .Select(c => Database.StripName(c)) + .ToArray(); + + var columnName = parts.Last(); + + // Get table name from mapper + string tableName = table; + + if (string.IsNullOrEmpty(tableName)) + { + tableName = (mapper != null && mapper.Table != null) ? mapper.Table.Name : string.Empty; + + if (parts.Length > 1 && string.IsNullOrEmpty(tableName)) + { + // OK, we have a multi part identifier, that's good, we can get table name + tableName = string.Join(".", parts.Take(parts.Length - 1)); + } + } + + // Try to get table info from cache + var tableInfo = !string.IsNullOrEmpty(tableName) ? + Tables.FirstOrDefault(x => !string.IsNullOrEmpty(x.Alias) && x.Alias.ToLower() == tableName) ?? + Tables.FirstOrDefault(x => x.Name.ToLower() == tableName.ToLower()) ?? Tables.FirstOrDefault() : + this is DynamicModifyBuilder ? Tables.FirstOrDefault() : null; + + // Try to get column from schema + if (tableInfo != null && tableInfo.Schema != null) + return tableInfo.Schema.TryGetNullable(columnName.ToLower()); + + // Well, we failed to find a column + return null; + } + + #endregion Helpers + } + + /// Implementation of dynamic select query builder. + internal class DynamicSelectQueryBuilder : DynamicQueryBuilder, IDynamicSelectQueryBuilder, DynamicQueryBuilder.IQueryWithWhere + { + private int? _top = null; + private int? _limit = null; + private int? _offset = null; + private bool _distinct = false; + + private string _select; + private string _from; + private string _join; + private string _groupby; + private string _orderby; + + /// + /// Gets a value indicating whether this instance has select columns. + /// + public bool HasSelectColumns { get { return !string.IsNullOrEmpty(_select); } } + + /// + /// Initializes a new instance of the class. + /// + /// The database. + public DynamicSelectQueryBuilder(DynamicDatabase db) + : base(db) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The database. + /// The parent query. + internal DynamicSelectQueryBuilder(DynamicDatabase db, DynamicQueryBuilder parent) + : base(db, parent) + { + } + + /// Generates the text this command will execute against the underlying database. + /// The text to execute against the underlying database. + public override string CommandText() + { + StringBuilder sb = new StringBuilder("SELECT"); + if (_distinct) sb.AppendFormat(" DISTINCT"); + if (_top.HasValue) sb.AppendFormat(" TOP {0}", _top); + if (_select != null) sb.AppendFormat(" {0}", _select); else sb.Append(" *"); + if (_from != null) sb.AppendFormat(" FROM {0}", _from); + if (_join != null) sb.AppendFormat(" {0}", _join); + if (WhereCondition != null) sb.AppendFormat(" WHERE {0}", WhereCondition); + if (_groupby != null) sb.AppendFormat(" GROUP BY {0}", _groupby); + if (_orderby != null) sb.AppendFormat(" ORDER BY {0}", _orderby); + if (_limit.HasValue) sb.AppendFormat(" LIMIT {0}", _limit); + if (_offset.HasValue) sb.AppendFormat(" OFFSET {0}", _offset); + + return sb.ToString(); + } + + #region Execution + + /*/// Execute this builder. + + /// Enumerator of objects expanded from query. + public virtual IEnumerator GetEnumerator() + { + using (var con = Database.Open()) + using (var cmd = con.CreateCommand()) + { + using (var rdr = cmd + .SetCommand(this) + .ExecuteReader()) + while (rdr.Read()) + { + dynamic val = null; + + // Work around to avoid yield being in try...catchblock: + // http://stackoverflow.com/questions/346365/why-cant-yield-return-appear-inside-a-try-block-with-a-catch + try + { + val = rdr.RowToDynamic(); + } + catch (ArgumentException argex) + { + var sb = new StringBuilder(); + cmd.Dump(sb); + + throw new ArgumentException(string.Format("{0}{1}{2}", argex.Message, Environment.NewLine, sb), + argex.InnerException.NullOr(a => a, argex)); + } + + yield return val; + } + } + } + + /// Execute this builder. + /// Enumerator of objects expanded from query. + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + }*/ + + /// Execute this builder. + /// Enumerator of objects expanded from query. + public virtual IEnumerable Execute() + { + using (var con = Database.Open()) + using (var cmd = con.CreateCommand()) + using (var rdr = cmd + .SetCommand(this) + .ExecuteReader()) + while (rdr.Read()) + { + dynamic val = null; + + // Work around to avoid yield being in try...catchblock: + // http://stackoverflow.com/questions/346365/why-cant-yield-return-appear-inside-a-try-block-with-a-catch + try + { + val = rdr.RowToDynamic(); + } + catch (ArgumentException argex) + { + var sb = new StringBuilder(); + cmd.Dump(sb); + + throw new ArgumentException(string.Format("{0}{1}{2}", argex.Message, Environment.NewLine, sb), + argex.InnerException.NullOr(a => a, argex)); + } + + yield return val; + } + } + + /// Execute this builder and map to given type. + /// Type of object to map on. + /// Enumerator of objects expanded from query. + public virtual IEnumerable Execute() where T : class + { + var mapper = DynamicMapperCache.GetMapper(); + + if (mapper == null) + throw new InvalidOperationException("Type can't be mapped for unknown reason."); + + using (var con = Database.Open()) + using (var cmd = con.CreateCommand()) + { + using (var rdr = cmd + .SetCommand(this) + .ExecuteReader()) + while (rdr.Read()) + { + dynamic val = null; + + // Work around to avoid yield being in try...catchblock: + // http://stackoverflow.com/questions/346365/why-cant-yield-return-appear-inside-a-try-block-with-a-catch + try + { + val = rdr.RowToDynamic(); + } + catch (ArgumentException argex) + { + var sb = new StringBuilder(); + cmd.Dump(sb); + + throw new ArgumentException(string.Format("{0}{1}{2}", argex.Message, Environment.NewLine, sb), + argex.InnerException.NullOr(a => a, argex)); + } + + yield return mapper.Create(val) as T; + } + } + } + + /// Execute this builder as a data reader. + /// Action containing reader. + public virtual void ExecuteDataReader(Action reader) + { + using (var con = Database.Open()) + using (var cmd = con.CreateCommand()) + using (var rdr = cmd + .SetCommand(this) + .ExecuteReader()) + reader(rdr); + } + + /// Returns a single result. + /// Result of a query. + public virtual object Scalar() + { + using (var con = Database.Open()) + using (var cmd = con.CreateCommand()) + { + return cmd + .SetCommand(this) + .ExecuteScalar(); + } + } + + #endregion Execution + + #region From/Join + + /// + /// Adds to the 'From' clause the contents obtained by parsing the dynamic lambda expressions given. The supported + /// formats are: + /// - Resolve to a string: 'x => "Table AS Alias', where the alias part is optional. + /// - Resolve to an expression: 'x => x.Table.As( x.Alias )', where the alias part is optional. + /// - Generic expression: 'x => x( expression ).As( x.Alias )', where the alias part is mandatory. In this + /// case the alias is not annotated. + /// + /// The specification. + /// The specification. + /// This instance to permit chaining. + public virtual IDynamicSelectQueryBuilder From(Func fn, params Func[] func) + { + if (fn == null) + throw new ArgumentNullException("Array of functions cannot be or contain null."); + + int index = FromFunc(-1, fn); + foreach (var f in func) + index = FromFunc(index, f); + + return this; + } + + private int FromFunc(int index, Func f) + { + if (f == null) + throw new ArgumentNullException("Array of functions cannot be or contain null."); + + index++; + ITableInfo tableInfo = null; + using (var parser = DynamicParser.Parse(f)) + { + var result = parser.Result; + + // If the expression result is string. + if (result is string) + { + var node = (string)result; + var tuple = node.SplitSomethingAndAlias(); + var parts = tuple.Item1.Split('.'); + tableInfo = new TableInfo(Database, + Database.StripName(parts.Last()).Validated("Table"), + tuple.Item2.Validated("Alias", canbeNull: true), + parts.Length == 2 ? Database.StripName(parts.First()).Validated("Owner", canbeNull: true) : null); + } + else if (result is Type) + { + Type type = (Type)result; + if (type.IsAnonymous()) + throw new InvalidOperationException(string.Format("Cant assign anonymous type as a table ({0}). Parsing {1}", type.FullName, result)); + + var mapper = DynamicMapperCache.GetMapper(type); + + if (mapper == null) + throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}). Parsing {1}", type.FullName, result)); + + tableInfo = new TableInfo(Database, type); + } + else if (result is DynamicParser.Node) + { + // Or if it resolves to a dynamic node + var node = (DynamicParser.Node)result; + + string owner = null; + string main = null; + string alias = null; + Type type = null; + + while (true) + { + // Support for the AS() virtual method... + if (node is DynamicParser.Node.Method && ((DynamicParser.Node.Method)node).Name.ToUpper() == "AS") + { + if (alias != null) + throw new ArgumentException(string.Format("Alias '{0}' is already set when parsing '{1}'.", alias, result)); + + object[] args = ((DynamicParser.Node.Method)node).Arguments; + + if (args == null) + throw new ArgumentNullException("arg", "AS() is not a parameterless method."); + + if (args.Length != 1) + throw new ArgumentException("AS() requires one and only one parameter: " + args.Sketch()); + + alias = Parse(args[0], rawstr: true, decorate: false).Validated("Alias"); + + node = node.Host; + continue; + } + + /*if (node is DynamicParser.Node.Method && ((DynamicParser.Node.Method)node).Name.ToUpper() == "subquery") + { + main = Parse(this.SubQuery(((DynamicParser.Node.Method)node).Arguments.Where(p => p is Func).Cast>().ToArray()), Parameters); + continue; + }*/ + + // Support for table specifications... + if (node is DynamicParser.Node.GetMember) + { + if (owner != null) + throw new ArgumentException(string.Format("Owner '{0}.{1}' is already set when parsing '{2}'.", owner, main, result)); + + if (main != null) + owner = ((DynamicParser.Node.GetMember)node).Name; + else + main = ((DynamicParser.Node.GetMember)node).Name; + + node = node.Host; + continue; + } + + // Support for generic sources... + if (node is DynamicParser.Node.Invoke) + { + if (owner != null) + throw new ArgumentException(string.Format("Owner '{0}.{1}' is already set when parsing '{2}'.", owner, main, result)); + + if (main != null) + owner = string.Format("{0}", Parse(node, rawstr: true, pars: Parameters)); + else + { + var invoke = (DynamicParser.Node.Invoke)node; + if (invoke.Arguments.Length == 1 && invoke.Arguments[0] is Type) + { + type = (Type)invoke.Arguments[0]; + if (type.IsAnonymous()) + throw new InvalidOperationException(string.Format("Cant assign anonymous type as a table ({0}). Parsing {1}", type.FullName, result)); + + var mapper = DynamicMapperCache.GetMapper(type); + + if (mapper == null) + throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}). Parsing {1}", type.FullName, result)); + + main = mapper.Table == null || string.IsNullOrEmpty(mapper.Table.Name) ? + mapper.Type.Name : mapper.Table.Name; + + owner = (mapper.Table != null) ? mapper.Table.Owner : owner; + } + else + main = string.Format("{0}", Parse(node, rawstr: true, pars: Parameters)); + } + + node = node.Host; + continue; + } + + // Just finished the parsing... + if (node is DynamicParser.Node.Argument) break; + + // All others are assumed to be part of the main element... + if (main != null) + main = Parse(node, pars: Parameters); + else + main = Parse(node, pars: Parameters); + + break; + } + + if (!string.IsNullOrEmpty(main)) + tableInfo = type == null ? new TableInfo(Database, main, alias, owner) : new TableInfo(Database, type, alias, owner); + else + throw new ArgumentException(string.Format("Specification #{0} is invalid: {1}", index, result)); + } + + // Or it is a not supported expression... + if (tableInfo == null) + throw new ArgumentException(string.Format("Specification #{0} is invalid: {1}", index, result)); + + Tables.Add(tableInfo); + + // We finally add the contents... + StringBuilder sb = new StringBuilder(); + + if (!string.IsNullOrEmpty(tableInfo.Owner)) + sb.AppendFormat("{0}.", Database.DecorateName(tableInfo.Owner)); + + sb.Append(tableInfo.Name.ContainsAny(StringExtensions.InvalidMemberChars) ? tableInfo.Name : Database.DecorateName(tableInfo.Name)); + + if (!string.IsNullOrEmpty(tableInfo.Alias)) + sb.AppendFormat(" AS {0}", tableInfo.Alias); + + _from = string.IsNullOrEmpty(_from) ? sb.ToString() : string.Format("{0}, {1}", _from, sb.ToString()); + } + return index; + } + + /// + /// Adds to the 'Join' clause the contents obtained by parsing the dynamic lambda expressions given. The supported + /// formats are: + /// - Resolve to a string: 'x => "Table AS Alias ON Condition', where the alias part is optional. + /// - Resolve to an expression: 'x => x.Table.As( x.Alias ).On( condition )', where the alias part is optional. + /// - Generic expression: 'x => x( expression ).As( x.Alias ).On( condition )', where the alias part is mandatory. + /// In this case the alias is not annotated. + /// The expression might be prepended by a method that, in this case, is used to specify the specific join type you + /// want to perform, as in: 'x => x.Left()...". Two considerations apply: + /// - If a 'false' argument is used when no 'Join' part appears in its name, then no 'Join' suffix is added + /// with a space in between. + /// - If a 'false' argument is used when a 'Join' part does appear, then no split is performed to separate the + /// 'Join' part. + /// + /// The specification. + /// This instance to permit chaining. + public virtual IDynamicSelectQueryBuilder Join(params Func[] func) + { + // We need to do two passes to add aliases first. + return JoinInternal(true, func).JoinInternal(false, func); + } + + /// + /// Adds to the 'Join' clause the contents obtained by parsing the dynamic lambda expressions given. The supported + /// formats are: + /// - Resolve to a string: 'x => "Table AS Alias ON Condition', where the alias part is optional. + /// - Resolve to an expression: 'x => x.Table.As( x.Alias ).On( condition )', where the alias part is optional. + /// - Generic expression: 'x => x( expression ).As( x.Alias ).On( condition )', where the alias part is mandatory. + /// In this case the alias is not annotated. + /// The expression might be prepended by a method that, in this case, is used to specify the specific join type you + /// want to perform, as in: 'x => x.Left()...". Two considerations apply: + /// - If a 'false' argument is used when no 'Join' part appears in its name, then no 'Join' suffix is added + /// with a space in between. + /// - If a 'false' argument is used when a 'Join' part does appear, then no split is performed to separate the + /// 'Join' part. + /// + /// If true just pass by to locate tables and aliases, otherwise create rules. + /// The specification. + /// This instance to permit chaining. + protected virtual DynamicSelectQueryBuilder JoinInternal(bool justAddTables, params Func[] func) + { + if (func == null) throw new ArgumentNullException("Array of functions cannot be null."); + + int index = -1; + + foreach (var f in func) + { + index++; + ITableInfo tableInfo = null; + + if (f == null) + throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); + + using (var parser = DynamicParser.Parse(f)) + { + var result = parser.Result; + if (result == null) throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); + + string type = null; + string main = null; + string owner = null; + string alias = null; + string condition = null; + Type tableType = null; + + // If the expression resolves to a string... + if (result is string) + { + var node = (string)result; + + int n = node.ToUpper().IndexOf("JOIN "); + + if (n < 0) + main = node; + else + { + // For strings we only accept 'JOIN' as a suffix + type = node.Substring(0, n + 4); + main = node.Substring(n + 4); + } + + n = main.ToUpper().IndexOf("ON"); + + if (n >= 0) + { + condition = main.Substring(n + 3); + main = main.Substring(0, n).Trim(); + } + + var tuple = main.SplitSomethingAndAlias(); // In this case we split on the remaining 'main' + var parts = tuple.Item1.Split('.'); + main = Database.StripName(parts.Last()).Validated("Table"); + owner = parts.Length == 2 ? Database.StripName(parts.First()).Validated("Owner", canbeNull: true) : null; + alias = tuple.Item2.Validated("Alias", canbeNull: true); + } + else if (result is DynamicParser.Node) + { + // Or if it resolves to a dynamic node... + var node = (DynamicParser.Node)result; + while (true) + { + // Support for the ON() virtual method... + if (node is DynamicParser.Node.Method && ((DynamicParser.Node.Method)node).Name.ToUpper() == "ON") + { + if (condition != null) + throw new ArgumentException(string.Format("Condition '{0}' is already set when parsing '{1}'.", alias, result)); + + object[] args = ((DynamicParser.Node.Method)node).Arguments; + if (args == null) + throw new ArgumentNullException("arg", "ON() is not a parameterless method."); + + if (args.Length != 1) + throw new ArgumentException("ON() requires one and only one parameter: " + args.Sketch()); + + condition = Parse(args[0], rawstr: true, pars: justAddTables ? null : Parameters); + + node = node.Host; + continue; + } + + // Support for the AS() virtual method... + if (node is DynamicParser.Node.Method && ((DynamicParser.Node.Method)node).Name.ToUpper() == "AS") + { + if (alias != null) + throw new ArgumentException(string.Format("Alias '{0}' is already set when parsing '{1}'.", alias, result)); + + object[] args = ((DynamicParser.Node.Method)node).Arguments; + + if (args == null) + throw new ArgumentNullException("arg", "AS() is not a parameterless method."); + + if (args.Length != 1) + throw new ArgumentException("AS() requires one and only one parameter: " + args.Sketch()); + + alias = Parse(args[0], rawstr: true, decorate: false, isMultiPart: false).Validated("Alias"); + + node = node.Host; + continue; + } + + // Support for table specifications... + if (node is DynamicParser.Node.GetMember) + { + if (owner != null) + throw new ArgumentException(string.Format("Owner '{0}.{1}' is already set when parsing '{2}'.", owner, main, result)); + + if (main != null) + owner = ((DynamicParser.Node.GetMember)node).Name; + else + main = ((DynamicParser.Node.GetMember)node).Name; + + node = node.Host; + continue; + } + + // Support for Join Type specifications... + if (node is DynamicParser.Node.Method && (node.Host is DynamicParser.Node.Argument || node.Host is DynamicParser.Node.Invoke)) + { + if (type != null) throw new ArgumentException(string.Format("Join type '{0}' is already set when parsing '{1}'.", main, result)); + type = ((DynamicParser.Node.Method)node).Name; + + bool avoid = false; + object[] args = ((DynamicParser.Node.Method)node).Arguments; + + if (args != null && args.Length > 0) + { + avoid = args[0] is bool && !((bool)args[0]); + var proposedType = args.FirstOrDefault(a => a is string) as string; + if (!string.IsNullOrEmpty(proposedType)) + type = proposedType; + } + + type = type.ToUpper(); // Normalizing, and stepping out the trivial case... + if (type != "JOIN") + { + // Special cases + // x => x.LeftOuter() / x => x.RightOuter()... + type = type.Replace("OUTER", " OUTER ") + .Replace(" ", " ") + .Trim(' '); + + // x => x.Left()... + int n = type.IndexOf("JOIN"); + + if (n < 0 && !avoid) + type += " JOIN"; + + // x => x.InnerJoin() / x => x.JoinLeft() ... + else + { + if (!avoid) + { + if (n == 0) type = type.Replace("JOIN", "JOIN "); + else type = type.Replace("JOIN", " JOIN"); + } + } + } + + node = node.Host; + continue; + } + + // Support for generic sources... + if (node is DynamicParser.Node.Invoke) + { + if (owner != null) + throw new ArgumentException(string.Format("Owner '{0}.{1}' is already set when parsing '{2}'.", owner, main, result)); + + if (main != null) + owner = string.Format("{0}", Parse(node, rawstr: true, pars: justAddTables ? null : Parameters)); + else + { + var invoke = (DynamicParser.Node.Invoke)node; + if (invoke.Arguments.Length == 1 && invoke.Arguments[0] is Type) + { + tableType = (Type)invoke.Arguments[0]; + var mapper = DynamicMapperCache.GetMapper(tableType); + + if (mapper == null) + throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}).", tableType.FullName)); + + main = mapper.Table == null || string.IsNullOrEmpty(mapper.Table.Name) ? + mapper.Type.Name : mapper.Table.Name; + + owner = (mapper.Table != null) ? mapper.Table.Owner : owner; + } + else + main = string.Format("{0}", Parse(node, rawstr: true, pars: justAddTables ? null : Parameters)); + } + + node = node.Host; + continue; + } + + // Just finished the parsing... + if (node is DynamicParser.Node.Argument) break; + throw new ArgumentException(string.Format("Specification #{0} is invalid: {1}", index, result)); + } + } + else + { + // Or it is a not supported expression... + throw new ArgumentException(string.Format("Specification #{0} is invalid: {1}", index, result)); + } + + // We annotate the aliases being conservative... + main = main.Validated("Main"); + + if (justAddTables) + { + if (!string.IsNullOrEmpty(main)) + tableInfo = tableType == null ? new TableInfo(Database, main, alias, owner) : new TableInfo(Database, tableType, alias, owner); + else + throw new ArgumentException(string.Format("Specification #{0} is invalid: {1}", index, result)); + + Tables.Add(tableInfo); + } + else + { + // Get cached table info + tableInfo = string.IsNullOrEmpty(alias) ? + Tables.SingleOrDefault(t => t.Name == main && string.IsNullOrEmpty(t.Alias)) : + Tables.SingleOrDefault(t => t.Alias == alias); + + // We finally add the contents if we can... + StringBuilder sb = new StringBuilder(); + if (string.IsNullOrEmpty(type)) + type = "JOIN"; + + sb.AppendFormat("{0} ", type); + + if (!string.IsNullOrEmpty(tableInfo.Owner)) + sb.AppendFormat("{0}.", Database.DecorateName(tableInfo.Owner)); + + sb.Append(tableInfo.Name.ContainsAny(StringExtensions.InvalidMemberChars) ? tableInfo.Name : Database.DecorateName(tableInfo.Name)); + + if (!string.IsNullOrEmpty(tableInfo.Alias)) + sb.AppendFormat(" AS {0}", tableInfo.Alias); + + if (!string.IsNullOrEmpty(condition)) + sb.AppendFormat(" ON {0}", condition); + + _join = string.IsNullOrEmpty(_join) ? sb.ToString() : string.Format("{0} {1}", _join, sb.ToString()); // No comma in this case + } + } + } + + return this; + } + + #endregion From/Join + + #region Where + + /// + /// Adds to the 'Where' clause the contents obtained from parsing the dynamic lambda expression given. The condition + /// is parsed to the appropriate syntax, where the specific customs virtual methods supported by the parser are used + /// as needed. + /// - If several Where() methods are chained their contents are, by default, concatenated with an 'AND' operator. + /// - The 'And()' and 'Or()' virtual method can be used to concatenate with an 'OR' or an 'AND' operator, as in: + /// 'Where( x => x.Or( condition ) )'. + /// + /// The specification. + /// This instance to permit chaining. + public virtual IDynamicSelectQueryBuilder Where(Func func) + { + return this.InternalWhere(func); + } + + /// Add where condition. + /// Condition column with operator and value. + /// Builder instance. + public virtual IDynamicSelectQueryBuilder Where(DynamicColumn column) + { + return this.InternalWhere(column); + } + + /// Add where condition. + /// Condition column. + /// Condition operator. + /// Condition value. + /// Builder instance. + public virtual IDynamicSelectQueryBuilder Where(string column, DynamicColumn.CompareOperator op, object value) + { + return this.InternalWhere(column, op, value); + } + + /// Add where condition. + /// Condition column. + /// Condition value. + /// Builder instance. + public virtual IDynamicSelectQueryBuilder Where(string column, object value) + { + return this.InternalWhere(column, value); + } + + /// Add where condition. + /// Set conditions as properties and values of an object. + /// If true use schema to determine key columns and ignore those which + /// aren't keys. + /// Builder instance. + public virtual IDynamicSelectQueryBuilder Where(object conditions, bool schema = false) + { + return this.InternalWhere(conditions, schema); + } + + #endregion Where + + #region Select + + /// + /// Adds to the 'Select' clause the contents obtained by parsing the dynamic lambda expressions given. The supported + /// formats are: + /// - Resolve to a string: 'x => "Table.Column AS Alias', where the alias part is optional. + /// - Resolve to an expression: 'x => x.Table.Column.As( x.Alias )', where the alias part is optional. + /// - Select all columns from a table: 'x => x.Table.All()'. + /// - Generic expression: 'x => x( expression ).As( x.Alias )', where the alias part is mandatory. In this case + /// the alias is not annotated. + /// + /// The specification. + /// The specification. + /// This instance to permit chaining. + public virtual IDynamicSelectQueryBuilder Select(Func fn, params Func[] func) + { + if (fn == null) + throw new ArgumentNullException("Array of specifications cannot be null."); + + int index = SelectFunc(-1, fn); + if (func != null) + foreach (var f in func) + index = SelectFunc(index, f); + + return this; + } + + private int SelectFunc(int index, Func f) + { + index++; + if (f == null) + throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); + + using (var parser = DynamicParser.Parse(f)) + { + var result = parser.Result; + if (result == null) + throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); + + string main = null; + string alias = null; + bool all = false; + bool anon = false; + + // If the expression resolves to a string... + if (result is string) + { + var node = (string)result; + var tuple = node.SplitSomethingAndAlias(); + main = tuple.Item1.Validated("Table and/or Column"); + + main = FixObjectName(main); + + alias = tuple.Item2.Validated("Alias", canbeNull: true); + } + else if (result is DynamicParser.Node) + { + // Or if it resolves to a dynamic node... + ParseSelectNode(result, ref main, ref alias, ref all); + } + else if (result.GetType().IsAnonymous()) + { + anon = true; + + foreach (var prop in result.ToDictionary()) + { + if (prop.Value is string) + { + var node = (string)prop.Value; + var tuple = node.SplitSomethingAndAlias(); + main = FixObjectName(tuple.Item1.Validated("Table and/or Column")); + + ////alias = tuple.Item2.Validated("Alias", canbeNull: true); + } + else if (prop.Value is DynamicParser.Node) + { + // Or if it resolves to a dynamic node... + ParseSelectNode(prop.Value, ref main, ref alias, ref all); + } + else + { + // Or it is a not supported expression... + throw new ArgumentException(string.Format("Specification #{0} in anonymous type is invalid: {1}", index, prop.Value)); + } + + alias = Database.DecorateName(prop.Key); + ParseSelectAddColumn(main, alias, all); + } + } + else + { + // Or it is a not supported expression... + throw new ArgumentException(string.Format("Specification #{0} is invalid: {1}", index, result)); + } + + if (!anon) + ParseSelectAddColumn(main, alias, all); + } + return index; + } + + /// Add select columns. + /// Columns to add to object. + /// Builder instance. + public virtual IDynamicSelectQueryBuilder SelectColumn(params DynamicColumn[] columns) + { + foreach (var col in columns) + Select(x => col.ToSQLSelectColumn(Database)); + + return this; + } + + /// Add select columns. + /// Columns to add to object. + /// Column format consist of Column Name, Alias and + /// Aggregate function in this order separated by ':'. + /// Builder instance. + public virtual IDynamicSelectQueryBuilder SelectColumn(params string[] columns) + { + return SelectColumn(columns.Select(c => DynamicColumn.ParseSelectColumn(c)).ToArray()); + } + + #endregion Select + + #region GroupBy + + /// + /// Adds to the 'Group By' clause the contents obtained from from parsing the dynamic lambda expression given. + /// + /// The specification. + /// The specification. + /// This instance to permit chaining. + public virtual IDynamicSelectQueryBuilder GroupBy(Func fn, params Func[] func) + { + if (fn == null) + throw new ArgumentNullException("Array of specifications cannot be null."); + + int index = GroupByFunc(-1, fn); + + if (func != null) + foreach (var f in func) + index = GroupByFunc(index, f); + + return this; + } + + private int GroupByFunc(int index, Func f) + { + index++; + if (f == null) + throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); + using (var parser = DynamicParser.Parse(f)) + { + var result = parser.Result; + if (result == null) + throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); + + string main = null; + + if (result is string) + main = FixObjectName(result as string); + else + main = Parse(result, pars: Parameters); + + main = main.Validated("Group By"); + if (_groupby == null) + _groupby = main; + else + _groupby = string.Format("{0}, {1}", _groupby, main); + } + return index; + } + + /// Add select columns. + /// Columns to group by. + /// Builder instance. + public virtual IDynamicSelectQueryBuilder GroupByColumn(params DynamicColumn[] columns) + { + foreach (var col in columns) + GroupBy(x => col.ToSQLGroupByColumn(Database)); + + return this; + } + + /// Add select columns. + /// Columns to group by. + /// Column format consist of Column Name and + /// Alias in this order separated by ':'. + /// Builder instance. + public virtual IDynamicSelectQueryBuilder GroupByColumn(params string[] columns) + { + return GroupByColumn(columns.Select(c => DynamicColumn.ParseSelectColumn(c)).ToArray()); + } + + #endregion GroupBy + + #region OrderBy + + /// + /// Adds to the 'Order By' clause the contents obtained from from parsing the dynamic lambda expression given. It + /// accepts a multipart column specification followed by an optional Ascending() or Descending() virtual methods + /// to specify the direction. If no virtual method is used, the default is ascending order. You can also use the + /// shorter versions Asc() and Desc(). + /// + /// The specification. + /// The specification. + /// This instance to permit chaining. + public virtual IDynamicSelectQueryBuilder OrderBy(Func fn, params Func[] func) + { + if (fn == null) + throw new ArgumentNullException("Array of specifications cannot be null."); + + int index = OrderByFunc(-1, fn); + + if (func != null) + foreach (var f in func) + index = OrderByFunc(index, f); + + return this; + } + + private int OrderByFunc(int index, Func f) + { + index++; + if (f == null) + throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); + + using (var parser = DynamicParser.Parse(f)) + { + var result = parser.Result; + if (result == null) throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); + + string main = null; + bool ascending = true; + + if (result is int) + main = result.ToString(); + else if (result is string) + { + var parts = ((string)result).Split(' '); + main = Database.StripName(parts.First()); + + int colNo; + if (!Int32.TryParse(main, out colNo)) + main = FixObjectName(main); + + ascending = parts.Length != 2 || parts.Last().ToUpper() == "ASCENDING" || parts.Last().ToUpper() == "ASC"; + } + else + { + // Intercepting trailing 'Ascending' or 'Descending' virtual methods... + if (result is DynamicParser.Node.Method) + { + var node = (DynamicParser.Node.Method)result; + var name = node.Name.ToUpper(); + if (name == "ASCENDING" || name == "ASC" || name == "DESCENDING" || name == "DESC") + { + object[] args = node.Arguments; + if (args != null && !(node.Host is DynamicParser.Node.Argument)) + throw new ArgumentException(string.Format("{0} must be a parameterless method, but found: {1}.", name, args.Sketch())); + else if ((args == null || args.Length != 1) && node.Host is DynamicParser.Node.Argument) + throw new ArgumentException(string.Format("{0} requires one numeric parameter, but found: {1}.", name, args.Sketch())); + + ascending = (name == "ASCENDING" || name == "ASC") ? true : false; + + if (args != null && args.Length == 1) + { + int col = -1; + if (args[0] is int) + main = args[0].ToString(); + else if (args[0] is string) + { + if (Int32.TryParse(args[0].ToString(), out col)) + main = col.ToString(); + else + main = FixObjectName(args[0].ToString()); + } + else + main = Parse(args[0], pars: Parameters); + } + + result = node.Host; + } + } + + // Just parsing the contents... + if (!(result is DynamicParser.Node.Argument)) + main = Parse(result, pars: Parameters); + } + + main = main.Validated("Order By"); + main = string.Format("{0} {1}", main, ascending ? "ASC" : "DESC"); + + if (_orderby == null) + _orderby = main; + else + _orderby = string.Format("{0}, {1}", _orderby, main); + } + return index; + } + + /// Add select columns. + /// Columns to order by. + /// Builder instance. + public virtual IDynamicSelectQueryBuilder OrderByColumn(params DynamicColumn[] columns) + { + foreach (var col in columns) + OrderBy(x => col.ToSQLOrderByColumn(Database)); + + return this; + } + + /// Add select columns. + /// Columns to order by. + /// Column format consist of Column Name and + /// Alias in this order separated by ':'. + /// Builder instance. + public virtual IDynamicSelectQueryBuilder OrderByColumn(params string[] columns) + { + return OrderByColumn(columns.Select(c => DynamicColumn.ParseOrderByColumn(c)).ToArray()); + } + + #endregion OrderBy + + #region Top/Limit/Offset/Distinct + + /// Set top if database support it. + /// How many objects select. + /// Builder instance. + public virtual IDynamicSelectQueryBuilder Top(int? top) + { + if ((Database.Options & DynamicDatabaseOptions.SupportTop) != DynamicDatabaseOptions.SupportTop) + throw new NotSupportedException("Database doesn't support TOP clause."); + + _top = top; + return this; + } + + /// Set top if database support it. + /// How many objects select. + /// Builder instance. + public virtual IDynamicSelectQueryBuilder Limit(int? limit) + { + if ((Database.Options & DynamicDatabaseOptions.SupportLimitOffset) != DynamicDatabaseOptions.SupportLimitOffset) + throw new NotSupportedException("Database doesn't support LIMIT clause."); + + _limit = limit; + return this; + } + + /// Set top if database support it. + /// How many objects skip selecting. + /// Builder instance. + public virtual IDynamicSelectQueryBuilder Offset(int? offset) + { + if ((Database.Options & DynamicDatabaseOptions.SupportLimitOffset) != DynamicDatabaseOptions.SupportLimitOffset) + throw new NotSupportedException("Database doesn't support OFFSET clause."); + + _offset = offset; + return this; + } + + /// Set distinct mode. + /// Distinct mode. + /// Builder instance. + public virtual IDynamicSelectQueryBuilder Distinct(bool distinct = true) + { + _distinct = distinct; + return this; + } + + #endregion Top/Limit/Offset/Distinct + + #region Helpers + + private void ParseSelectAddColumn(string main, string alias, bool all) + { + // We annotate the aliases being conservative... + main = main.Validated("Main"); + + ////if (alias != null && !main.ContainsAny(StringExtensions.InvalidMemberChars)) TableAliasList.Add(new KTableAlias(main, alias)); + + // If all columns are requested... + if (all) + main += ".*"; + + // We finally add the contents... + string str = (alias == null || all) ? main : string.Format("{0} AS {1}", main, alias); + _select = _select == null ? str : string.Format("{0}, {1}", _select, str); + } + + private void ParseSelectNode(object result, ref string column, ref string alias, ref bool all) + { + string main = null; + + var node = (DynamicParser.Node)result; + while (true) + { + // Support for the AS() virtual method... + if (node is DynamicParser.Node.Method && ((DynamicParser.Node.Method)node).Name.ToUpper() == "AS") + { + if (alias != null) + throw new ArgumentException(string.Format("Alias '{0}' is already set when parsing '{1}'.", alias, result)); + + object[] args = ((DynamicParser.Node.Method)node).Arguments; + + if (args == null) + throw new ArgumentNullException("arg", "AS() is not a parameterless method."); + + if (args.Length != 1) + throw new ArgumentException("AS() requires one and only one parameter: " + args.Sketch()); + + // Yes, we decorate columns + alias = Parse(args[0], rawstr: true, decorate: true, isMultiPart: false).Validated("Alias"); + + node = node.Host; + continue; + } + + // Support for the ALL() virtual method... + if (node is DynamicParser.Node.Method && ((DynamicParser.Node.Method)node).Name.ToUpper() == "ALL") + { + if (all) + throw new ArgumentException(string.Format("Flag to select all columns is already set when parsing '{0}'.", result)); + + object[] args = ((DynamicParser.Node.Method)node).Arguments; + + if (args != null) + throw new ArgumentException("ALL() must be a parameterless virtual method, but found: " + args.Sketch()); + + all = true; + + node = node.Host; + continue; + } + + // Support for table and/or column specifications... + if (node is DynamicParser.Node.GetMember) + { + if (main != null) + throw new ArgumentException(string.Format("Main '{0}' is already set when parsing '{1}'.", main, result)); + + main = ((DynamicParser.Node.GetMember)node).Name; + + if (node.Host is DynamicParser.Node.GetMember) + { + // If leaf then decorate + main = Database.DecorateName(main); + + // Supporting multipart specifications... + node = node.Host; + + // Get table/alias name + var table = ((DynamicParser.Node.GetMember)node).Name; + bool isAlias = node.Host is DynamicParser.Node.Argument && IsTableAlias(table); + + if (isAlias) + main = string.Format("{0}.{1}", table, main); + else if (node.Host is DynamicParser.Node.GetMember) + { + node = node.Host; + main = string.Format("{0}.{1}.{2}", + Database.DecorateName(((DynamicParser.Node.GetMember)node).Name), + Database.DecorateName(table), main); + } + else + main = string.Format("{0}.{1}", Database.DecorateName(table), main); + } + else if (node.Host is DynamicParser.Node.Argument) + { + var table = ((DynamicParser.Node.Argument)node.Host).Name; + + if (IsTableAlias(table)) + main = string.Format("{0}.{1}", table, Database.DecorateName(main)); + else if (!IsTableAlias(main)) + main = Database.DecorateName(main); + } + else if (!(node.Host is DynamicParser.Node.Argument && IsTableAlias(main))) + main = Database.DecorateName(main); + + node = node.Host; + + continue; + } + + // Support for generic sources... + if (node is DynamicParser.Node.Invoke) + { + if (main != null) + throw new ArgumentException(string.Format("Main '{0}' is already set when parsing '{1}'.", main, result)); + + main = string.Format("{0}", Parse(node, rawstr: true, pars: Parameters)); + + node = node.Host; + continue; + } + + // Just finished the parsing... + if (node is DynamicParser.Node.Argument) + { + if (string.IsNullOrEmpty(main) && IsTableAlias(node.Name)) + main = node.Name; + + break; + } + + // All others are assumed to be part of the main element... + if (main != null) throw new ArgumentException(string.Format("Main '{0}' is already set when parsing '{1}'.", main, result)); + main = Parse(node, pars: Parameters); + + break; + } + + column = main; + } + + #endregion Helpers + } + + /// Update query builder. + internal class DynamicUpdateQueryBuilder : DynamicModifyBuilder, IDynamicUpdateQueryBuilder, DynamicQueryBuilder.IQueryWithWhere + { + private string _columns; + + internal DynamicUpdateQueryBuilder(DynamicDatabase db) + : base(db) + { + } + + public DynamicUpdateQueryBuilder(DynamicDatabase db, string tableName) + : base(db, tableName) + { + } + + /// Generates the text this command will execute against the underlying database. + /// The text to execute against the underlying database. + /// This method must be override by derived classes. + public override string CommandText() + { + var info = Tables.Single(); + return string.Format("UPDATE {0}{1} SET {2}{3}{4}", + string.IsNullOrEmpty(info.Owner) ? string.Empty : string.Format("{0}.", Database.DecorateName(info.Owner)), + Database.DecorateName(info.Name), _columns, + string.IsNullOrEmpty(WhereCondition) ? string.Empty : " WHERE ", + WhereCondition); + } + + #region Update + + /// Add update value or where condition using schema. + /// Update or where column name. + /// Column value. + /// Builder instance. + public virtual IDynamicUpdateQueryBuilder Update(string column, object value) + { + DynamicSchemaColumn? col = GetColumnFromSchema(column); + + if (!col.HasValue && SupportSchema) + throw new InvalidOperationException(string.Format("Column '{0}' not found in schema, can't use universal approach.", column)); + + if (col.HasValue && col.Value.IsKey) + Where(column, value); + else + Values(column, value); + + return this; + } + + /// Add update values and where condition columns using schema. + /// Set values or conditions as properties and values of an object. + /// Builder instance. + public virtual IDynamicUpdateQueryBuilder Update(object conditions) + { + if (conditions is DynamicColumn) + { + var column = (DynamicColumn)conditions; + + DynamicSchemaColumn? col = column.Schema ?? GetColumnFromSchema(column.ColumnName); + + if (!col.HasValue && SupportSchema) + throw new InvalidOperationException(string.Format("Column '{0}' not found in schema, can't use universal approach.", column)); + + if (col.HasValue && col.Value.IsKey) + Where(column); + else + Values(column.ColumnName, column.Value); + + return this; + } + + var dict = conditions.ToDictionary(); + var mapper = DynamicMapperCache.GetMapper(conditions.GetType()); + + foreach (var con in dict) + { + if (mapper.Ignored.Contains(con.Key)) + continue; + + string colName = mapper != null ? mapper.PropertyMap.TryGetValue(con.Key) ?? con.Key : con.Key; + DynamicSchemaColumn? col = GetColumnFromSchema(colName); + + if (!col.HasValue && SupportSchema) + throw new InvalidOperationException(string.Format("Column '{0}' not found in schema, can't use universal approach.", colName)); + + if (col.HasValue) + { + colName = col.Value.Name; + + if (col.Value.IsKey) + { + Where(colName, con.Value); + + continue; + } + } + + Values(colName, con.Value); + } + + return this; + } + + #endregion Update + + #region Values + + /// + /// Specifies the columns to update using the dynamic lambda expressions given. Each expression correspond to one + /// column, and can: + /// - Resolve to a string, in this case a '=' must appear in the string. + /// - Resolve to a expression with the form: 'x => x.Column = Value'. + /// + /// The specifications. + /// This instance to permit chaining. + public virtual IDynamicUpdateQueryBuilder Set(params Func[] func) + { + if (func == null) + throw new ArgumentNullException("Array of specifications cannot be null."); + + int index = -1; + foreach (var f in func) + { + index++; + if (f == null) + throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); + var result = DynamicParser.Parse(f).Result; + + if (result == null) + throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); + + string main = null; + string value = null; + string str = null; + + // When 'x => x.Table.Column = value' or 'x => x.Column = value'... + if (result is DynamicParser.Node.SetMember) + { + var node = (DynamicParser.Node.SetMember)result; + + DynamicSchemaColumn? col = GetColumnFromSchema(node.Name); + main = Database.DecorateName(node.Name); + value = Parse(node.Value, pars: Parameters, nulls: true, columnSchema: col); + + str = string.Format("{0} = {1}", main, value); + _columns = _columns == null ? str : string.Format("{0}, {1}", _columns, str); + continue; + } + else if (!(result is DynamicParser.Node) && !result.GetType().IsValueType) + { + Values(result); + continue; + } + + // Other specifications are considered invalid... + var err = string.Format("Specification '{0}' is invalid.", result); + str = Parse(result); + if (str.Contains("=")) err += " May have you used a '==' instead of a '=' operator?"; + throw new ArgumentException(err); + } + + return this; + } + + /// Add insert fields. + /// Insert column. + /// Insert value. + /// Builder instance. + public virtual IDynamicUpdateQueryBuilder Values(string column, object value) + { + if (value is DynamicColumn) + { + var v = (DynamicColumn)value; + + if (string.IsNullOrEmpty(v.ColumnName)) + v.ColumnName = column; + + return Values(v); + } + + return Values(new DynamicColumn + { + ColumnName = column, + Value = value, + }); + } + + /// Add insert fields. + /// Set insert value as properties and values of an object. + /// Builder instance. + public virtual IDynamicUpdateQueryBuilder Values(object o) + { + if (o is DynamicColumn) + { + var column = (DynamicColumn)o; + DynamicSchemaColumn? col = column.Schema ?? GetColumnFromSchema(column.ColumnName); + + string main = FixObjectName(column.ColumnName, onlyColumn: true); + string value = Parse(column.Value, pars: Parameters, nulls: true, columnSchema: col); + + var str = string.Format("{0} = {1}", main, value); + _columns = _columns == null ? str : string.Format("{0}, {1}", _columns, str); + + return this; + } + + var dict = o.ToDictionary(); + var mapper = DynamicMapperCache.GetMapper(o.GetType()); + + if (mapper != null) + { + foreach (var con in dict) + if (!mapper.Ignored.Contains(con.Key)) + Values(mapper.PropertyMap.TryGetValue(con.Key) ?? con.Key, con.Value); + } + else + foreach (var con in dict) + Values(con.Key, con.Value); + + return this; + } + + #endregion Values + + #region Where + + /// + /// Adds to the 'Where' clause the contents obtained from parsing the dynamic lambda expression given. The condition + /// is parsed to the appropriate syntax, where the specific customs virtual methods supported by the parser are used + /// as needed. + /// - If several Where() methods are chained their contents are, by default, concatenated with an 'AND' operator. + /// - The 'And()' and 'Or()' virtual method can be used to concatenate with an 'OR' or an 'AND' operator, as in: + /// 'Where( x => x.Or( condition ) )'. + /// + /// The specification. + /// This instance to permit chaining. + public virtual IDynamicUpdateQueryBuilder Where(Func func) + { + return this.InternalWhere(func); + } + + /// Add where condition. + /// Condition column with operator and value. + /// Builder instance. + public virtual IDynamicUpdateQueryBuilder Where(DynamicColumn column) + { + return this.InternalWhere(column); + } + + /// Add where condition. + /// Condition column. + /// Condition operator. + /// Condition value. + /// Builder instance. + public virtual IDynamicUpdateQueryBuilder Where(string column, DynamicColumn.CompareOperator op, object value) + { + return this.InternalWhere(column, op, value); + } + + /// Add where condition. + /// Condition column. + /// Condition value. + /// Builder instance. + public virtual IDynamicUpdateQueryBuilder Where(string column, object value) + { + return this.InternalWhere(column, value); + } + + /// Add where condition. + /// Set conditions as properties and values of an object. + /// If true use schema to determine key columns and ignore those which + /// aren't keys. + /// Builder instance. + public virtual IDynamicUpdateQueryBuilder Where(object conditions, bool schema = false) + { + return this.InternalWhere(conditions, schema); + } + + #endregion Where + } + } + } + + namespace Helpers + { + /// Defines methods to support the comparison of collections for equality. + /// The type of collection to compare. + public class CollectionComparer : IEqualityComparer> + { + /// Determines whether the specified objects are equal. + /// The first object of type T to compare. + /// The second object of type T to compare. + /// Returns true if the specified objects are equal; otherwise, false. + bool IEqualityComparer>.Equals(IEnumerable first, IEnumerable second) + { + return Equals(first, second); + } + + /// Returns a hash code for the specified object. + /// The enumerable for which a hash code is to be returned. + /// A hash code for the specified object. + int IEqualityComparer>.GetHashCode(IEnumerable enumerable) + { + return GetHashCode(enumerable); + } + + /// Returns a hash code for the specified object. + /// The enumerable for which a hash code is to be returned. + /// A hash code for the specified object. + public static int GetHashCode(IEnumerable enumerable) + { + int hash = 17; + + foreach (T val in enumerable.OrderBy(x => x)) + hash = (hash * 23) + val.GetHashCode(); + + return hash; + } + + /// Determines whether the specified objects are equal. + /// The first object of type T to compare. + /// The second object of type T to compare. + /// Returns true if the specified objects are equal; otherwise, false. + public static bool Equals(IEnumerable first, IEnumerable second) + { + if ((first == null) != (second == null)) + return false; + + if (!object.ReferenceEquals(first, second) && (first != null)) + { + if (first.Count() != second.Count()) + return false; + + if ((first.Count() != 0) && HaveMismatchedElement(first, second)) + return false; + } + + return true; + } + + private static bool HaveMismatchedElement(IEnumerable first, IEnumerable second) + { + int firstCount; + int secondCount; + + var firstElementCounts = GetElementCounts(first, out firstCount); + var secondElementCounts = GetElementCounts(second, out secondCount); + + if (firstCount != secondCount) + return true; + + foreach (var kvp in firstElementCounts) + if (kvp.Value != (secondElementCounts.TryGetNullable(kvp.Key) ?? 0)) + return true; + + return false; + } + + private static Dictionary GetElementCounts(IEnumerable enumerable, out int nullCount) + { + var dictionary = new Dictionary(); + nullCount = 0; + + foreach (T element in enumerable) + { + if (element == null) + nullCount++; + else + { + int count = dictionary.TryGetNullable(element) ?? 0; + dictionary[element] = ++count; + } + } + + return dictionary; + } + } + + /// Framework detection and specific implementations. + public static class FrameworkTools + { + #region Mono or .NET Framework detection + + /// This is pretty simple trick. + private static bool _isMono = Type.GetType("Mono.Runtime") != null; + + /// Gets a value indicating whether application is running under mono runtime. + public static bool IsMono { get { return _isMono; } } + + #endregion Mono or .NET Framework detection + + static FrameworkTools() + { + _frameworkTypeArgumentsGetter = CreateTypeArgumentsGetter(); + } + + #region GetGenericTypeArguments + + private static Func> _frameworkTypeArgumentsGetter = null; + + private static Func> CreateTypeArgumentsGetter() + { + // HACK: Creating binders assuming types are correct... this may fail. + if (IsMono) + { + var binderType = typeof(Microsoft.CSharp.RuntimeBinder.RuntimeBinderException).Assembly.GetType("Microsoft.CSharp.RuntimeBinder.CSharpInvokeMemberBinder"); + + if (binderType != null) + { + ParameterExpression param = Expression.Parameter(typeof(InvokeMemberBinder), "o"); + + return Expression.Lambda>>( + Expression.TypeAs( + Expression.Field( + Expression.TypeAs(param, binderType), "typeArguments"), + typeof(IList)), param).Compile(); + } + } + else + { + var inter = typeof(Microsoft.CSharp.RuntimeBinder.RuntimeBinderException).Assembly.GetType("Microsoft.CSharp.RuntimeBinder.ICSharpInvokeOrInvokeMemberBinder"); + + if (inter != null) + { + var prop = inter.GetProperty("TypeArguments"); + + if (!prop.CanRead) + return null; + + var objParm = Expression.Parameter(typeof(InvokeMemberBinder), "o"); + + return Expression.Lambda>>( + Expression.TypeAs( + Expression.Property( + Expression.TypeAs(objParm, inter), prop.Name), + typeof(IList)), objParm).Compile(); + } + } + + return null; + } + + /// Extension method allowing to easily extract generic type + /// arguments from assuming that it + /// inherits from + /// Microsoft.CSharp.RuntimeBinder.ICSharpInvokeOrInvokeMemberBinder + /// in .NET Framework or + /// Microsoft.CSharp.RuntimeBinder.CSharpInvokeMemberBinder + /// under Mono. + /// Binder from which get type arguments. + /// This is generally a bad solution, but there is no other + /// currently so we have to go with it. + /// List of types passed as generic parameters. + public static IList GetGenericTypeArguments(this InvokeMemberBinder binder) + { + // First try to use delegate if exist + if (_frameworkTypeArgumentsGetter != null) + return _frameworkTypeArgumentsGetter(binder); + + if (_isMono) + { + // HACK: Using Reflection + // In mono this is trivial. + + // First we get field info. + var field = binder.GetType().GetField("typeArguments", BindingFlags.Instance | + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + + // If this was a success get and return it's value + if (field != null) + return field.GetValue(binder) as IList; + } + else + { + // HACK: Using Reflection + // In this case, we need more aerobic :D + + // First, get the interface + var inter = binder.GetType().GetInterface("Microsoft.CSharp.RuntimeBinder.ICSharpInvokeOrInvokeMemberBinder"); + + if (inter != null) + { + // Now get property. + var prop = inter.GetProperty("TypeArguments"); + + // If we have a property, return it's value + if (prop != null) + return prop.GetValue(binder, null) as IList; + } + } + + // Sadly return null if failed. + return null; + } + + #endregion GetGenericTypeArguments + } + + /// Extends interface. + public interface IExtendedDisposable : IDisposable + { + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// + /// true if this instance is disposed; otherwise, false. + /// + bool IsDisposed { get; } + } + + /// Class containing useful string extensions. + internal static class StringExtensions + { + static StringExtensions() + { + InvalidMultipartMemberChars = _InvalidMultipartMemberChars.ToCharArray(); + InvalidMemberChars = _InvalidMemberChars.ToCharArray(); + } + + private static readonly string _InvalidMultipartMemberChars = " +-*/^%[]{}()!\"\\&=?¿"; + private static readonly string _InvalidMemberChars = "." + _InvalidMultipartMemberChars; + + /// + /// Gets an array with some invalid characters that cannot be used with multipart names for class members. + /// + public static char[] InvalidMultipartMemberChars { get; private set; } + + /// + /// Gets an array with some invalid characters that cannot be used with names for class members. + /// + public static char[] InvalidMemberChars { get; private set; } + + /// + /// Provides with an alternate and generic way to obtain an alternate string representation for this instance, + /// applying the following rules: + /// - Null values are returned as with the value, or a null object. + /// - Enum values are translated into their string representation. + /// - If the type has override the 'ToString' method then it is used. + /// - If it is a dictionary, then a collection of key/value pairs where the value part is also translated. + /// - If it is a collection, then a collection of value items also translated. + /// - If it has public public properties (or if not, if it has public fields), the collection of name/value + /// pairs, with the values translated. + /// - Finally it falls back to the standard 'type.FullName' mechanism. + /// + /// The object to obtain its alternate string representation from. + /// The brackets to use if needed. If not null it must be at least a 2-chars' array containing + /// the opening and closing brackets. + /// Representation of null string.. + /// The alternate string representation of this object. + public static string Sketch(this object obj, char[] brackets = null, string nullString = "(null)") + { + if (obj == null) return nullString; + if (obj is string) return (string)obj; + + Type type = obj.GetType(); + if (type.IsEnum) return obj.ToString(); + + // If the ToString() method has been overriden (by the type itself, or by its parents), let's use it... + MethodInfo method = type.GetMethod("ToString", Type.EmptyTypes); + if (method.DeclaringType != typeof(object)) return obj.ToString(); + + // For alll other cases... + StringBuilder sb = new StringBuilder(); + bool first = true; + + // Dictionaries... + if (obj is IDictionary) + { + if (brackets == null || brackets.Length < 2) + brackets = "[]".ToCharArray(); + + sb.AppendFormat("{0}", brackets[0]); first = true; foreach (DictionaryEntry kvp in (IDictionary)obj) + { + if (!first) sb.Append(", "); else first = false; + sb.AppendFormat("'{0}'='{1}'", kvp.Key.Sketch(), kvp.Value.Sketch()); + } + + sb.AppendFormat("{0}", brackets[1]); + return sb.ToString(); + } + + // IEnumerables... + IEnumerator ator = null; + if (obj is IEnumerable) + ator = ((IEnumerable)obj).GetEnumerator(); + else + { + method = type.GetMethod("GetEnumerator", Type.EmptyTypes); + if (method != null) + ator = (IEnumerator)method.Invoke(obj, null); + } + + if (ator != null) + { + if (brackets == null || brackets.Length < 2) brackets = "[]".ToCharArray(); + sb.AppendFormat("{0}", brackets[0]); first = true; while (ator.MoveNext()) + { + if (!first) sb.Append(", "); else first = false; + sb.AppendFormat("{0}", ator.Current.Sketch()); + } + + sb.AppendFormat("{0}", brackets[1]); + + if (ator is IDisposable) + ((IDisposable)ator).Dispose(); + + return sb.ToString(); + } + + // As a last resort, using the public properties (or fields if needed, or type name)... + BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy; + PropertyInfo[] props = type.GetProperties(flags); + FieldInfo[] infos = type.GetFields(flags); + + if (props.Length == 0 && infos.Length == 0) sb.Append(type.FullName); // Fallback if needed + else + { + if (brackets == null || brackets.Length < 2) brackets = "{}".ToCharArray(); + sb.AppendFormat("{0}", brackets[0]); + first = true; + + if (props.Length != 0) + { + foreach (var prop in props) + { + if (!first) sb.Append(", "); else first = false; + sb.AppendFormat("{0}='{1}'", prop.Name, prop.GetValue(obj, null).Sketch()); + } + } + else + { + if (infos.Length != 0) + { + foreach (var info in infos) + { + if (!first) sb.Append(", "); else first = false; + sb.AppendFormat("{0}='{1}'", info.Name, info.GetValue(obj).Sketch()); + } + } + } + + sb.AppendFormat("{0}", brackets[1]); + } + + // And returning... + return sb.ToString(); + } + + /// + /// Returns true if the target string contains any of the characters given. + /// + /// The target string. It cannot be null. + /// An array containing the characters to test. It cannot be null. If empty false is returned. + /// True if the target string contains any of the characters given, false otherwise. + public static bool ContainsAny(this string source, char[] items) + { + if (source == null) throw new ArgumentNullException("source", "Source string cannot be null."); + if (items == null) throw new ArgumentNullException("items", "Array of characters to test cannot be null."); + + if (items.Length == 0) return false; // No characters to validate + int ix = source.IndexOfAny(items); + return ix >= 0 ? true : false; + } + + /// + /// Returns a new validated string using the rules given. + /// + /// The source string. + /// A description of the source string to build errors and exceptions if needed. + /// True if the returned string can be null. + /// True if the returned string can be empty. + /// True to trim the returned string. + /// True to left-trim the returned string. + /// True to right-trim the returned string. + /// If >= 0, the min valid length for the returned string. + /// If >= 0, the max valid length for the returned string. + /// If not '\0', the character to use to left-pad the returned string if needed. + /// If not '\0', the character to use to right-pad the returned string if needed. + /// If not null, an array containing invalid chars that must not appear in the returned + /// string. + /// If not null, an array containing the only characters that are considered valid for the + /// returned string. + /// A new validated string. + public static string Validated(this string source, string desc = null, + bool canbeNull = false, bool canbeEmpty = false, + bool trim = true, bool trimStart = false, bool trimEnd = false, + int minLen = -1, int maxLen = -1, char padLeft = '\0', char padRight = '\0', + char[] invalidChars = null, char[] validChars = null) + { + // Assuring a valid descriptor... + if (string.IsNullOrWhiteSpace(desc)) desc = "Source"; + + // Validating if null sources are accepted... + if (source == null) + { + if (!canbeNull) throw new ArgumentNullException(desc, string.Format("{0} cannot be null.", desc)); + return null; + } + + // Trimming if needed... + if (trim && !(trimStart || trimEnd)) source = source.Trim(); + else + { + if (trimStart) source = source.TrimStart(' '); + if (trimEnd) source = source.TrimEnd(' '); + } + + // Adjusting lenght... + if (minLen > 0) + { + if (padLeft != '\0') source = source.PadLeft(minLen, padLeft); + if (padRight != '\0') source = source.PadRight(minLen, padRight); + } + + if (maxLen > 0) + { + if (padLeft != '\0') source = source.PadLeft(maxLen, padLeft); + if (padRight != '\0') source = source.PadRight(maxLen, padRight); + } + + // Validating emptyness and lenghts... + if (source.Length == 0) + { + if (!canbeEmpty) throw new ArgumentException(string.Format("{0} cannot be empty.", desc)); + return string.Empty; + } + + if (minLen >= 0 && source.Length < minLen) throw new ArgumentException(string.Format("Lenght of {0} '{1}' is lower than '{2}'.", desc, source, minLen)); + if (maxLen >= 0 && source.Length > maxLen) throw new ArgumentException(string.Format("Lenght of {0} '{1}' is bigger than '{2}'.", desc, source, maxLen)); + + // Checking invalid chars... + if (invalidChars != null) + { + int n = source.IndexOfAny(invalidChars); + if (n >= 0) throw new ArgumentException(string.Format("Invalid character '{0}' found in {1} '{2}'.", source[n], desc, source)); + } + + // Checking valid chars... + if (validChars != null) + { + int n = validChars.ToString().IndexOfAny(source.ToCharArray()); + if (n >= 0) throw new ArgumentException(string.Format("Invalid character '{0}' found in {1} '{2}'.", validChars.ToString()[n], desc, source)); + } + + return source; + } + + /// + /// Splits the given string with the 'something AS alias' format, returning a tuple containing its 'something' and 'alias' parts. + /// If no alias is detected, then its component in the tuple returned is null and all the contents from the source + /// string are considered as the 'something' part. + /// + /// The source string. + /// A tuple containing the 'something' and 'alias' parts. + public static Tuple SplitSomethingAndAlias(this string source) + { + source = source.Validated("[Something AS Alias]"); + + string something = null; + string alias = null; + int n = source.LastIndexOf(" AS ", StringComparison.OrdinalIgnoreCase); + + if (n < 0) + something = source; + else + { + something = source.Substring(0, n); + alias = source.Substring(n + 4); + } + + return new Tuple(something, alias); + } + + /// Allows to replace parameters inside of string. + /// String containing parameters in format [$ParameterName]. + /// Function that should return value that will be placed in string in place of placed parameter. + /// Prefix of the parameter. This value can't be null or empty, default value [$. + /// Suffix of the parameter. This value can't be null or empty, default value ]. + /// Parsed string. + public static string FillStringWithVariables(this string stringToFill, Func getValue, string prefix = "[$", string sufix = "]") + { + int startPos = 0, endPos = 0; + prefix.Validated(); + sufix.Validated(); + + startPos = stringToFill.IndexOf(prefix, startPos); + while (startPos >= 0) + { + endPos = stringToFill.IndexOf(sufix, startPos + prefix.Length); + int nextStartPos = stringToFill.IndexOf(prefix, startPos + prefix.Length); + + if (endPos > startPos + prefix.Length + 1 && (nextStartPos > endPos || nextStartPos == -1)) + { + string paramName = stringToFill.Substring(startPos + prefix.Length, endPos - (startPos + prefix.Length)); + + stringToFill = stringToFill + .Remove(startPos, (endPos - startPos) + sufix.Length) + .Insert(startPos, getValue(paramName)); + } + + startPos = stringToFill.IndexOf(prefix, startPos + prefix.Length); + } + + return stringToFill; + } + } + + /// Class contains unclassified extensions. + internal static class UnclassifiedExtensions + { + /// Easy way to use conditional value. + /// Includes . + /// Input object type to check. + /// Result type. + /// The object to check. + /// The select function. + /// The else value. + /// Selected value or default value. + /// It lets you do this: + /// var lname = thingy.NullOr(t => t.Name).NullOr(n => n.ToLower()); + /// which is more fluent and (IMO) easier to read than this: + /// var lname = (thingy != null ? thingy.Name : null) != null ? thingy.Name.ToLower() : null; + /// + public static R NullOr(this T obj, Func func, R elseValue = default(R)) where T : class + { + return obj != null && obj != DBNull.Value ? + func(obj) : elseValue; + } + + /// Easy way to use conditional value. + /// Includes . + /// Input object type to check. + /// Result type. + /// The object to check. + /// The select function. + /// The else value function. + /// Selected value or default value. + /// It lets you do this: + /// var lname = thingy.NullOr(t => t.Name).NullOr(n => n.ToLower()); + /// which is more fluent and (IMO) easier to read than this: + /// var lname = (thingy != null ? thingy.Name : null) != null ? thingy.Name.ToLower() : null; + /// + public static R NullOrFn(this T obj, Func func, Func elseFunc = null) where T : class + { + // Old if to avoid recurency. + return obj != null && obj != DBNull.Value ? + func(obj) : elseFunc != null ? elseFunc() : default(R); + } + } + + namespace Dynamics + { + /// + /// Class able to parse dynamic lambda expressions. Allows to create dynamic logic. + /// + public class DynamicParser : IExtendedDisposable + { + #region Node + + /// + /// Generic bindable operation where some of its operands is a dynamic argument, or a dynamic member or + /// a method of that argument. + /// + [Serializable] + public class Node : IDynamicMetaObjectProvider, IExtendedDisposable, ISerializable + { + #region MetaNode + + /// + /// Represents the dynamic binding and a binding logic of + /// an object participating in the dynamic binding. + /// + internal class MetaNode : DynamicMetaObject + { + /// + /// Initializes a new instance of the class. + /// + /// The parameter. + /// The restrictions. + /// The value. + public MetaNode(Expression parameter, BindingRestrictions rest, object value) + : base(parameter, rest, value) + { + } + + private DynamicMetaObject GetBinder(Func newNodeFunc) + { + var o = (Node)this.Value; + var node = newNodeFunc(o); + o.Parser.Last = node; + + var p = Expression.Variable(typeof(Node), "ret"); + var exp = Expression.Block(new ParameterExpression[] { p }, Expression.Assign(p, Expression.Constant(node))); + + return new MetaNode(exp, this.Restrictions, node); + } + + /// + /// Performs the binding of the dynamic get member operation. + /// + /// An instance of the that represents the details of the dynamic operation. + /// + /// The new representing the result of the binding. + /// + public override DynamicMetaObject BindGetMember(GetMemberBinder binder) + { + return GetBinder(x => new GetMember(x, binder.Name) { Parser = x.Parser }); + } + + /// + /// Performs the binding of the dynamic set member operation. + /// + /// An instance of the that represents the details of the dynamic operation. + /// The representing the value for the set member operation. + /// + /// The new representing the result of the binding. + /// + public override DynamicMetaObject BindSetMember(SetMemberBinder binder, DynamicMetaObject value) + { + return GetBinder(x => new SetMember(x, binder.Name, value.Value) { Parser = x.Parser }); + } + + /// + /// Performs the binding of the dynamic get index operation. + /// + /// An instance of the that represents the details of the dynamic operation. + /// An array of instances - indexes for the get index operation. + /// + /// The new representing the result of the binding. + /// + public override DynamicMetaObject BindGetIndex(GetIndexBinder binder, DynamicMetaObject[] indexes) + { + return GetBinder(x => new GetIndex(x, indexes.Select(m => m.Value).ToArray()) { Parser = x.Parser }); + } + + /// + /// Performs the binding of the dynamic set index operation. + /// + /// An instance of the that represents the details of the dynamic operation. + /// An array of instances - indexes for the set index operation. + /// The representing the value for the set index operation. + /// + /// The new representing the result of the binding. + /// + public override DynamicMetaObject BindSetIndex(SetIndexBinder binder, DynamicMetaObject[] indexes, DynamicMetaObject value) + { + return GetBinder(x => new SetIndex(x, indexes.Select(m => m.Value).ToArray(), value.Value) { Parser = x.Parser }); + } + + /// + /// Performs the binding of the dynamic invoke operation. + /// + /// An instance of the that represents the details of the dynamic operation. + /// An array of instances - arguments to the invoke operation. + /// + /// The new representing the result of the binding. + /// + public override DynamicMetaObject BindInvoke(InvokeBinder binder, DynamicMetaObject[] args) + { + return GetBinder(x => new Invoke(x, args.Select(m => m.Value).ToArray()) { Parser = x.Parser }); + } + + /// + /// Performs the binding of the dynamic invoke member operation. + /// + /// An instance of the that represents the details of the dynamic operation. + /// An array of instances - arguments to the invoke member operation. + /// + /// The new representing the result of the binding. + /// + public override DynamicMetaObject BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args) + { + return GetBinder(x => new Method(x, binder.Name, args.Select(m => m.Value).ToArray()) { Parser = x.Parser }); + } + + /// + /// Performs the binding of the dynamic binary operation. + /// + /// An instance of the that represents the details of the dynamic operation. + /// An instance of the representing the right hand side of the binary operation. + /// + /// The new representing the result of the binding. + /// + public override DynamicMetaObject BindBinaryOperation(BinaryOperationBinder binder, DynamicMetaObject arg) + { + return GetBinder(x => new Binary(x, binder.Operation, arg.Value) { Parser = x.Parser }); + } + + /// + /// Performs the binding of the dynamic unary operation. + /// + /// An instance of the that represents the details of the dynamic operation. + /// + /// The new representing the result of the binding. + /// + public override DynamicMetaObject BindUnaryOperation(UnaryOperationBinder binder) + { + var o = (Node)this.Value; + var node = new Unary(o, binder.Operation) { Parser = o.Parser }; + o.Parser.Last = node; + + // If operation is 'IsTrue' or 'IsFalse', we will return false to keep the engine working... + object ret = node; + if (binder.Operation == ExpressionType.IsTrue) ret = (object)false; + if (binder.Operation == ExpressionType.IsFalse) ret = (object)false; + + var p = Expression.Variable(ret.GetType(), "ret"); // the type is now obtained from "ret" + var exp = Expression.Block( + new ParameterExpression[] { p }, + Expression.Assign(p, Expression.Constant(ret))); // the expression is now obtained from "ret" + + return new MetaNode(exp, this.Restrictions, node); + } + + /// + /// Performs the binding of the dynamic conversion operation. + /// + /// An instance of the that represents the details of the dynamic operation. + /// + /// The new representing the result of the binding. + /// + public override DynamicMetaObject BindConvert(ConvertBinder binder) + { + var o = (Node)this.Value; + var node = new Convert(o, binder.ReturnType) { Parser = o.Parser }; + o.Parser.Last = node; + + // Reducing the object to return if this is an assignment node... + object ret = o; + bool done = false; + + while (!done) + { + if (ret is SetMember) + ret = ((SetMember)o).Value; + else if (ret is SetIndex) + ret = ((SetIndex)o).Value; + else + done = true; + } + + // Creating an instance... + if (binder.ReturnType == typeof(string)) ret = ret.ToString(); + else + { + try + { + if (binder.ReturnType.IsNullableType()) + ret = null; // to avoid cast exceptions + else + ret = Activator.CreateInstance(binder.ReturnType, true); // true to allow non-public ctor as well + } + catch + { + // as the last resort scenario + ret = new object(); + } + } + + var p = Expression.Variable(binder.ReturnType, "ret"); + var exp = Expression.Block( + new ParameterExpression[] { p }, + Expression.Assign(p, Expression.Constant(ret, binder.ReturnType))); // specifying binder.ReturnType + + return new MetaNode(exp, this.Restrictions, node); + } + } + + #endregion MetaNode + + #region Argument + + /// + /// Describe a dynamic argument used in a dynamic lambda expression. + /// + [Serializable] + public class Argument : Node, ISerializable + { + /// + /// Initializes a new instance of the class. + /// + /// The name. + public Argument(string name) + : base(name) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The info. + /// The context. + protected Argument(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + /// Returns a that represents this instance. + /// A that represents this instance. + public override string ToString() + { + if (IsDisposed) + return "{DynamicParser::Node::Argument::Disposed}"; + return Name; + } + } + + #endregion Argument + + #region GetMember + + /// + /// Describe a 'get member' operation, as in 'x => x.Member'. + /// + [Serializable] + public class GetMember : Node, ISerializable + { + /// + /// Initializes a new instance of the class. + /// + /// The host. + /// The name. + public GetMember(Node host, string name) + : base(host, name) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The info. + /// The context. + protected GetMember(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + /// Returns a that represents this instance. + /// A that represents this instance. + public override string ToString() + { + if (IsDisposed) + return "{DynamicParser::Node::GetMember::Disposed}"; + return string.Format("{0}.{1}", Host.Sketch(), Name.Sketch()); + } + } + + #endregion GetMember + + #region SetMember + + /// + /// Describe a 'set member' operation, as in 'x => x.Member = y'. + /// + [Serializable] + public class SetMember : Node, ISerializable + { + /// + /// Gets the value that has been (virtually) assigned to this member. It might be null if the null value has been + /// assigned to this instance, or if this instance is disposed. + /// + public object Value { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The host. + /// The name. + /// The value. + public SetMember(Node host, string name, object value) + : base(host, name) + { + Value = value; + } + + /// + /// Initializes a new instance of the class. + /// + /// The info. + /// The context. + protected SetMember(SerializationInfo info, StreamingContext context) + : base(info, context) + { + string type = info.GetString("MemberType"); + Value = type == "NULL" ? null : info.GetValue("MemberValue", Type.GetType(type)); + } + + /// + /// Gets the object data. + /// + /// The info. + /// The context. + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue("MemberType", Value == null ? "NULL" : Value.GetType().AssemblyQualifiedName); + if (Value != null) + info.AddValue("MemberValue", Value); + + base.GetObjectData(info, context); + } + + /// Returns a that represents this instance. + /// A that represents this instance. + public override string ToString() + { + if (IsDisposed) + return "{DynamicParser::Node::SetMember::Disposed}"; + return string.Format("({0}.{1} = {2})", Host.Sketch(), Name.Sketch(), Value.Sketch()); + } + } + + #endregion SetMember + + #region GetIndex + + /// + /// Describe a 'get indexed' operation, as in 'x => x.Member[...]'. + /// + [Serializable] + public class GetIndex : Node, ISerializable + { + /// Gets the indexes. + public object[] Indexes { get; internal set; } + + /// + /// Initializes a new instance of the class. + /// + /// The host. + /// The indexes. + /// Indexes array cannot be null. + /// Indexes array cannot be empty. + public GetIndex(Node host, object[] indexes) + : base(host) + { + if (indexes == null) + throw new ArgumentNullException("indexes", "Indexes array cannot be null."); + if (indexes.Length == 0) + throw new ArgumentException("Indexes array cannot be empty."); + + Indexes = indexes; + } + + /// + /// Initializes a new instance of the class. + /// + /// The info. + /// The context. + protected GetIndex(SerializationInfo info, StreamingContext context) + : base(info, context) + { + int count = (int)info.GetValue("IndexCount", typeof(int)); + + if (count != 0) + { + Indexes = new object[count]; for (int i = 0; i < count; i++) + { + string typeName = info.GetString("IndexType" + i); + object obj = typeName == "NULL" ? null : info.GetValue("IndexValue" + i, Type.GetType(typeName)); + Indexes[i] = obj; + } + } + } + + /// + /// Gets the object data. + /// + /// The info. + /// The context. + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + int count = Indexes == null ? 0 : Indexes.Length; info.AddValue("IndexCount", count); + for (int i = 0; i < count; i++) + { + info.AddValue("IndexType" + i, Indexes[i] == null ? "NULL" : Indexes[i].GetType().AssemblyQualifiedName); + if (Indexes[i] != null) info.AddValue("IndexValue" + i, Indexes[i]); + } + + base.GetObjectData(info, context); + } + + /// Returns a that represents this instance. + /// A that represents this instance. + public override string ToString() + { + if (IsDisposed) + return "{DynamicParser::Node::GetIndex::Disposed}"; + + return string.Format("{0}{1}", Host.Sketch(), Indexes == null ? "[empty]" : Indexes.Sketch()); + } + } + + #endregion GetIndex + + #region SetIndex + + /// + /// Describe a 'set indexed' operation, as in 'x => x.Member[...] = Value'. + /// + [Serializable] + public class SetIndex : GetIndex, ISerializable + { + /// + /// Gets the value that has been (virtually) assigned to this member. It might be null if the null value has been + /// assigned to this instance, or if this instance is disposed. + /// + public object Value { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The host. + /// The indexes. + /// The value. + public SetIndex(Node host, object[] indexes, object value) + : base(host, indexes) + { + Value = value; + } + + /// + /// Initializes a new instance of the class. + /// + /// The info. + /// The context. + protected SetIndex(SerializationInfo info, StreamingContext context) + : base(info, context) + { + string type = info.GetString("MemberType"); + Value = type == "NULL" ? null : info.GetValue("MemberValue", Type.GetType(type)); + } + + /// + /// Gets the object data. + /// + /// The info. + /// The context. + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue("MemberType", Value == null ? "NULL" : Value.GetType().AssemblyQualifiedName); + if (Value != null) info.AddValue("MemberValue", Value); + + base.GetObjectData(info, context); + } + + /// Returns a that represents this instance. + /// A that represents this instance. + public override string ToString() + { + if (IsDisposed) + return "{DynamicParser::Node::SetIndex::Disposed}"; + + return string.Format("({0}{1} = {2})", Host.Sketch(), Indexes == null ? "[empty]" : Indexes.Sketch(), Value.Sketch()); + } + } + + #endregion SetIndex + + #region Invoke + + /// + /// Describe a method invocation operation, as in 'x => x.Method(...)". + /// + [Serializable] + public class Invoke : Node, ISerializable + { + /// Gets the arguments. + public object[] Arguments { get; internal set; } + + /// + /// Initializes a new instance of the class. + /// + /// The host. + /// The arguments. + public Invoke(Node host, object[] arguments) + : base(host) + { + Arguments = arguments == null || arguments.Length == 0 ? null : arguments; + } + + /// + /// Initializes a new instance of the class. + /// + /// The info. + /// The context. + protected Invoke(SerializationInfo info, StreamingContext context) + : base(info, context) + { + int count = (int)info.GetValue("ArgumentCount", typeof(int)); + + if (count != 0) + { + Arguments = new object[count]; for (int i = 0; i < count; i++) + { + string typeName = info.GetString("ArgumentType" + i); + object obj = typeName == "NULL" ? null : info.GetValue("ArgumentValue" + i, Type.GetType(typeName)); + Arguments[i] = obj; + } + } + } + + /// + /// Gets the object data. + /// + /// The info. + /// The context. + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + int count = Arguments == null ? 0 : Arguments.Length; info.AddValue("ArgumentCount", count); + for (int i = 0; i < count; i++) + { + info.AddValue("ArgumentType" + i, Arguments[i] == null ? "NULL" : Arguments[i].GetType().AssemblyQualifiedName); + if (Arguments[i] != null) info.AddValue("ArgumentValue" + i, Arguments[i]); + } + + base.GetObjectData(info, context); + } + + /// Returns a that represents this instance. + /// A that represents this instance. + public override string ToString() + { + if (IsDisposed) + return "{DynamicParser::Node::Invoke::Disposed}"; + + return string.Format("{0}{1}", Host.Sketch(), Arguments == null ? "()" : Arguments.Sketch(brackets: "()".ToCharArray())); + } + } + + #endregion Invoke + + #region Method + + /// + /// Describe a method invocation operation, as in 'x => x.Method(...)". + /// + [Serializable] + public class Method : Node, ISerializable + { + /// Gets the arguments. + public object[] Arguments { get; internal set; } + + /// + /// Initializes a new instance of the class. + /// + /// The host. + /// The name. + /// The arguments. + public Method(Node host, string name, object[] arguments) + : base(host, name) + { + Arguments = arguments == null || arguments.Length == 0 ? null : arguments; + } + + /// + /// Initializes a new instance of the class. + /// + /// The info. + /// The context. + protected Method(SerializationInfo info, StreamingContext context) + : base(info, context) + { + int count = (int)info.GetValue("ArgumentCount", typeof(int)); + + if (count != 0) + { + Arguments = new object[count]; for (int i = 0; i < count; i++) + { + string typeName = info.GetString("ArgumentType" + i); + object obj = typeName == "NULL" ? null : info.GetValue("ArgumentValue" + i, Type.GetType(typeName)); + Arguments[i] = obj; + } + } + } + + /// + /// Gets the object data. + /// + /// The info. + /// The context. + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + int count = Arguments == null ? 0 : Arguments.Length; info.AddValue("ArgumentCount", count); + for (int i = 0; i < count; i++) + { + info.AddValue("ArgumentType" + i, Arguments[i] == null ? "NULL" : Arguments[i].GetType().AssemblyQualifiedName); + if (Arguments[i] != null) info.AddValue("ArgumentValue" + i, Arguments[i]); + } + + base.GetObjectData(info, context); + } + + /// Returns a that represents this instance. + /// A that represents this instance. + public override string ToString() + { + if (IsDisposed) + return "{DynamicParser::Node::Method::Disposed}"; + + return string.Format("{0}.{1}{2}", Host.Sketch(), Name.Sketch(), Arguments == null ? "()" : Arguments.Sketch(brackets: "()".ToCharArray())); + } + } + + #endregion Method + + #region Binary + + /// + /// Represents a binary operation between a dynamic element and an arbitrary object, including null ones, as in + /// 'x => (x && null)'. The left operand must be an instance of , whereas the right one + /// can be any object, including null values. + /// + [Serializable] + public class Binary : Node, ISerializable + { + /// Gets the operation. + public ExpressionType Operation { get; private set; } + + /// Gets host of the . + public Node Left { get { return Host; } } + + /// Gets the right side value. + public object Right { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The left. + /// The operation. + /// The right. + public Binary(Node left, ExpressionType operation, object right) + : base(left) + { + Operation = operation; + Right = right; + } + + /// + /// Initializes a new instance of the class. + /// + /// The info. + /// The context. + protected Binary(SerializationInfo info, StreamingContext context) + : base(info, context) + { + Operation = (ExpressionType)info.GetValue("Operation", typeof(ExpressionType)); + + string type = info.GetString("RightType"); + Right = type == "NULL" ? null : (Node)info.GetValue("RightItem", Type.GetType(type)); + } + + /// + /// Gets the object data. + /// + /// The info. + /// The context. + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue("Operation", Operation); + + info.AddValue("RightType", Right == null ? "NULL" : Right.GetType().AssemblyQualifiedName); + if (Right != null) + info.AddValue("RightItem", Right); + + base.GetObjectData(info, context); + } + + /// Returns a that represents this instance. + /// A that represents this instance. + public override string ToString() + { + if (IsDisposed) + return "{DynamicParser::Node::Binary::Disposed}"; + + return string.Format("({0} {1} {2})", Host.Sketch(), Operation, Right.Sketch()); + } + } + + #endregion Binary + + #region Unary + + /// + /// Represents an unary operation, as in 'x => !x'. The target must be a instance. There + /// is no distinction between pre- and post- version of the same operation. + /// + [Serializable] + public class Unary : Node, ISerializable + { + /// Gets the operation. + public ExpressionType Operation { get; private set; } + + /// Gets host of the . + public Node Target { get { return Host; } } + + /// + /// Initializes a new instance of the class. + /// + /// The target. + /// The operation. + public Unary(Node target, ExpressionType operation) + : base(target) + { + Operation = operation; + } + + /// + /// Initializes a new instance of the class. + /// + /// The info. + /// The context. + protected Unary(SerializationInfo info, StreamingContext context) + : base(info, context) + { + Operation = (ExpressionType)info.GetValue("Operation", typeof(ExpressionType)); + } + + /// + /// Gets the object data. + /// + /// The info. + /// The context. + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue("Operation", Operation); + + base.GetObjectData(info, context); + } + + /// Returns a that represents this instance. + /// A that represents this instance. + public override string ToString() + { + if (IsDisposed) + return "{DynamicParser::Node::Binary::Disposed}"; + + return string.Format("({0} {1})", Operation, Host.Sketch()); + } + } + + #endregion Unary + + #region Convert + + /// + /// Represents a conversion operation, as in 'x => (string)x'. + /// + [Serializable] + public class Convert : Node, ISerializable + { + /// Gets the new type to which value will be converted. + public Type NewType { get; private set; } + + /// Gets host of the . + public Node Target { get { return Host; } } + + /// + /// Initializes a new instance of the class. + /// + /// The target. + /// The new type. + public Convert(Node target, Type newType) + : base(target) + { + NewType = newType; + } + + /// + /// Initializes a new instance of the class. + /// + /// The info. + /// The context. + protected Convert(SerializationInfo info, StreamingContext context) + : base(info, context) + { + NewType = (Type)info.GetValue("NewType", typeof(Type)); + } + + /// + /// Gets the object data. + /// + /// The info. + /// The context. + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue("NewType", NewType); + + base.GetObjectData(info, context); + } + } + + #endregion Convert + + /// + /// Gets the name of the member. It might be null if this instance is disposed. + /// + public string Name { get; internal set; } + + /// Gets host of the . + public Node Host { get; internal set; } + + /// Gets reference to the parser. + public DynamicParser Parser { get; internal set; } + + /// + /// Initializes a new instance of the class. + /// + internal Node() + { + IsDisposed = false; + } + + /// + /// Initializes a new instance of the class. + /// + /// The host. + internal Node(Node host) + : this() + { + if (host == null) + throw new ArgumentNullException("host", "Host cannot be null."); + + Host = host; + } + + /// + /// Initializes a new instance of the class. + /// + /// The name. + internal Node(string name) + : this() + { + Name = name.Validated("Name"); + } + + /// + /// Initializes a new instance of the class. + /// + /// The host. + /// The name. + /// Host cannot be null. + internal Node(Node host, string name) + : this(host) + { + Name = name.Validated("Name"); + } + + /// + /// Initializes a new instance of the class. + /// + /// The info. + /// The context. + protected Node(SerializationInfo info, StreamingContext context) + { + Name = info.GetString("MemberName"); + + string type = info.GetString("HostType"); + Host = type == "NULL" ? null : (Node)info.GetValue("HostItem", Type.GetType(type)); + } + + /// Returns a that represents this instance. + /// A that represents this instance. + public override string ToString() + { + if (IsDisposed) + return "{DynamicParser::Node::Disposed}"; + + return "{DynamicParser::Node::Empty}"; + } + + #region Implementation of IDynamicMetaObjectProvider + + /// Returns the responsible + /// for binding operations performed on this object. + /// The expression tree representation of the runtime value. + /// The to bind this object. + /// Thrown if this instance is disposed. + public DynamicMetaObject GetMetaObject(System.Linq.Expressions.Expression parameter) + { + if (IsDisposed) + throw new ObjectDisposedException("DynamicParser.Node"); + + return new MetaNode( + parameter, + BindingRestrictions.GetInstanceRestriction(parameter, this), + this); + } + + #endregion Implementation of IDynamicMetaObjectProvider + + #region Implementation of IExtendedDisposable + + /// Gets a value indicating whether this instance is disposed. + public bool IsDisposed { get; private set; } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + IsDisposed = true; + } + + #endregion Implementation of IExtendedDisposable + + #region Implementation of ISerializable + + /// + /// Populates a with the data needed to serialize the target object. + /// + /// The to populate with data. + /// The destination (see ) for this serialization. + public virtual void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (!string.IsNullOrEmpty(Name)) + info.AddValue("MemberName", Name); + + info.AddValue("HostType", Host == null ? "NULL" : Host.GetType().AssemblyQualifiedName); + if (Host != null) + info.AddValue("HostItem", Host); + } + + #endregion Implementation of ISerializable + } + + #endregion Node + + #region Data + + private List _arguments = new List(); + private object _uncertainResult; + + #endregion Data + + #region Properties + + /// Gets the last node (root of the tree). + public Node Last { get; internal set; } + + /// + /// Gets an enumeration containing the dynamic arguments used in the dynamic lambda expression parsed. + /// + public IEnumerable Arguments + { + get + { + List list = new List(); + if (!IsDisposed && _arguments != null) + list.AddRange(_arguments); + + foreach (var arg in list) + yield return arg; + + list.Clear(); + list = null; + } + } + + /// + /// Gets the number of dynamic arguments used in the dynamic lambda expression parsed. + /// + public int Count + { + get { return _arguments == null ? 0 : _arguments.Count; } + } + + /// + /// Gets the result of the parsing of the dynamic lambda expression. This result can be either an arbitrary object, + /// including null, if the expression resolves to it, or an instance of the class that + /// contains the last logic expression evaluated when parsing the dynamic lambda expression. + /// + public object Result + { + get { return _uncertainResult ?? Last; } + } + + #endregion Properties + + private DynamicParser(Delegate f) + { + foreach (var p in f.Method.GetParameters()) + { + if (p.GetCustomAttributes(typeof(DynamicAttribute), true).Any()) + this._arguments.Add(new Node.Argument(p.Name) { Parser = this }); + else + throw new ArgumentException(string.Format("Argument '{0}' must be dynamic.", p.Name)); + } + + _uncertainResult = f.DynamicInvoke(_arguments.ToArray()); + } + + /// + /// Parses the dynamic lambda expression given in the form of a delegate, and returns a new instance of the + /// class that holds the dynamic arguments used in the dynamic lambda expression, and + /// the result of the parsing. + /// + /// The dynamic lambda expression to parse. + /// A new instance of . + public static DynamicParser Parse(Delegate f) + { + return new DynamicParser(f); + } + + /// Returns a that represents this instance. + /// A that represents this instance. + public override string ToString() + { + if (IsDisposed) + return "{DynamicParser::Disposed}"; + + StringBuilder sb = new StringBuilder(); + + sb.Append("("); + bool first = true; + + if (_arguments != null) + { + foreach (var arg in _arguments) + { + if (!first) sb.Append(", "); else first = false; + sb.Append(arg); + } + } + + sb.Append(")"); + + sb.AppendFormat(" => {0}", Result.Sketch()); + + return sb.ToString(); + } + + #region Implementation of IExtendedDisposable + + /// Gets a value indicating whether this instance is disposed. + public bool IsDisposed { get; private set; } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + IsDisposed = true; + } + + #endregion Implementation of IExtendedDisposable + } + + /// Class that allows to use interfaces as dynamic objects. + /// Type of class to proxy. + /// This is temporary solution. Which allows to use builders as a dynamic type. + public class DynamicProxy : DynamicObject + { + private T _proxy; + private Type _type; + private Dictionary _properties; + private Dictionary _methods; + + /// + /// Initializes a new instance of the class. + /// + /// The object to which proxy should be created. + /// The object to which proxy should be created is null. + public DynamicProxy(T proxiedObject) + { + if (proxiedObject == null) + throw new ArgumentNullException("proxiedObject"); + + _proxy = proxiedObject; + _type = typeof(T); + + _properties = _type.GetProperties() + .ToDictionary( + k => k.Name, + v => new DynamicPropertyInvoker(v, null)); + + _methods = _type.GetMethods() + .Where(m => !m.IsStatic && !m.IsGenericMethod) + .ToDictionary( + k => k, + v => + { + try + { + return Delegate.CreateDelegate(Expression.GetDelegateType(v.GetParameters().Select(t => t.ParameterType).Concat(new[] { v.ReflectedType }).ToArray()), _proxy, v.Name); + } + catch (ArgumentException) + { + return null; + } + }); + } + + /// Provides implementation for type conversion operations. + /// Classes derived from the + /// class can override this method to specify dynamic behavior for + /// operations that convert an object from one type to another. + /// Provides information about the conversion operation. + /// The binder.Type property provides the type to which the object must be + /// converted. For example, for the statement (String)sampleObject in C# + /// (CType(sampleObject, Type) in Visual Basic), where sampleObject is an + /// instance of the class derived from the + /// class, binder.Type returns the type. + /// The binder.Explicit property provides information about the kind of + /// conversion that occurs. It returns true for explicit conversion and + /// false for implicit conversion. + /// The result of the type conversion operation. + /// Returns true if the operation is successful; otherwise, false. + /// If this method returns false, the run-time binder of the language determines the + /// behavior. (In most cases, a language-specific run-time exception is thrown). + public override bool TryConvert(ConvertBinder binder, out object result) + { + if (binder.Type == typeof(T)) + { + result = _proxy; + return true; + } + + if (_proxy != null && + binder.Type.IsAssignableFrom(_proxy.GetType())) + { + result = _proxy; + return true; + } + + return base.TryConvert(binder, out result); + } + + /// Provides the implementation for operations that get member + /// values. Classes derived from the + /// class can override this method to specify dynamic behavior for + /// operations such as getting a value for a property. + /// Provides information about the object that + /// called the dynamic operation. The binder.Name property provides + /// the name of the member on which the dynamic operation is performed. + /// For example, for the Console.WriteLine(sampleObject.SampleProperty) + /// statement, where sampleObject is an instance of the class derived + /// from the class, + /// binder.Name returns "SampleProperty". The binder.IgnoreCase property + /// specifies whether the member name is case-sensitive. + /// The result of the get operation. For example, + /// if the method is called for a property, you can assign the property + /// value to . + /// Returns true if the operation is successful; otherwise, + /// false. If this method returns false, the run-time binder of the + /// language determines the behavior. (In most cases, a run-time exception + /// is thrown). + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + try + { + var prop = _properties.TryGetValue(binder.Name); + + result = prop.NullOr(p => p.Get.NullOr(g => g(_proxy), null), null); + + return prop != null && prop.Get != null; + } + catch (Exception ex) + { + throw new InvalidOperationException(string.Format("Cannot get member {0}", binder.Name), ex); + } + } + + /// Provides the implementation for operations that set member + /// values. Classes derived from the + /// class can override this method to specify dynamic behavior for operations + /// such as setting a value for a property. + /// Provides information about the object that called + /// the dynamic operation. The binder.Name property provides the name of + /// the member to which the value is being assigned. For example, for the + /// statement sampleObject.SampleProperty = "Test", where sampleObject is + /// an instance of the class derived from the + /// class, binder.Name returns "SampleProperty". The binder.IgnoreCase + /// property specifies whether the member name is case-sensitive. + /// The value to set to the member. For example, for + /// sampleObject.SampleProperty = "Test", where sampleObject is an instance + /// of the class derived from the + /// class, the is "Test". + /// Returns true if the operation is successful; otherwise, + /// false. If this method returns false, the run-time binder of the + /// language determines the behavior. (In most cases, a language-specific + /// run-time exception is thrown). + public override bool TrySetMember(SetMemberBinder binder, object value) + { + try + { + var prop = _properties.TryGetValue(binder.Name); + + if (prop != null && prop.Set != null) + { + prop.Set(_proxy, value); + return true; + } + + return false; + } + catch (Exception ex) + { + throw new InvalidOperationException(string.Format("Cannot set member {0} to '{1}'", binder.Name, value), ex); + } + } + + /// Provides the implementation for operations that invoke a member. + /// Classes derived from the + /// class can override this method to specify dynamic behavior for + /// operations such as calling a method. + /// Provides information about the dynamic operation. + /// The binder.Name property provides the name of the member on which the + /// dynamic operation is performed. For example, for the statement + /// sampleObject.SampleMethod(100), where sampleObject is an instance of + /// the class derived from the + /// class, binder.Name returns "SampleMethod". The binder.IgnoreCase property + /// specifies whether the member name is case-sensitive. + /// The arguments that are passed to the object member + /// during the invoke operation. For example, for the statement + /// sampleObject.SampleMethod(100), where sampleObject is derived from the + /// class, + /// First element of is equal to 100. + /// The result of the member invocation. + /// Returns true if the operation is successful; otherwise, + /// false. If this method returns false, the run-time binder of the + /// language determines the behavior. (In most cases, a language-specific + /// run-time exception is thrown). + public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) + { + return TryInvokeMethod(binder.Name, out result, args) || base.TryInvokeMember(binder, args, out result); + } + + private bool TryInvokeMethod(string name, out object result, object[] args) + { + result = null; + + MethodInfo mi = _methods.Keys + .Where(m => m.Name == name) + .FirstOrDefault(m => + CompareTypes(m.GetParameters().ToArray(), + args.Select(a => a.GetType()).ToArray())); + + Delegate d = _methods.TryGetValue(mi); + + if (d != null) + { + result = d.DynamicInvoke(CompleteArguments(mi.GetParameters().ToArray(), args)); + + if (d.Method.ReturnType == _type && result is T) + result = new DynamicProxy((T)result); + + return true; + } + else if (mi != null) + { + result = mi.Invoke(_proxy, CompleteArguments(mi.GetParameters().ToArray(), args)); + + if (mi.ReturnType == _type && result is T) + result = new DynamicProxy((T)result); + + return true; + } + + return false; + } + + private bool CompareTypes(ParameterInfo[] parameters, Type[] types) + { + if (parameters.Length < types.Length || parameters.Count(p => !p.IsOptional) > types.Length) + return false; + + for (int i = 0; i < types.Length; i++) + if (types[i] != parameters[i].ParameterType && !parameters[i].ParameterType.IsAssignableFrom(types[i])) + return false; + + return true; + } + + private object[] CompleteArguments(ParameterInfo[] parameters, object[] arguments) + { + return arguments.Concat(parameters.Skip(arguments.Length).Select(p => p.DefaultValue)).ToArray(); + } + } + } + } + + namespace Mapper + { + /// Allows to add table name to class. + [AttributeUsage(AttributeTargets.Property)] + public class ColumnAttribute : Attribute + { + /// Gets or sets name. + public string Name { get; set; } + + /// Gets or sets column type. + /// Used when overriding schema. + public DbType? Type { get; set; } + + /// Gets or sets a value indicating whether column is a key. + public bool IsKey { get; set; } + + /// Gets or sets a value indicating whether column should have unique value. + /// Used when overriding schema. + public bool? IsUnique { get; set; } + + /// Gets or sets column size. + /// Used when overriding schema. + public int? Size { get; set; } + + /// Gets or sets column precision. + /// Used when overriding schema. + public byte? Precision { get; set; } + + /// Gets or sets column scale. + /// Used when overriding schema. + public byte? Scale { get; set; } + + #region Constructors + + /// Initializes a new instance of the class. + public ColumnAttribute() + { + } + + /// Initializes a new instance of the class. + /// Name of column. + public ColumnAttribute(string name) + { + Name = name; + } + + /// Initializes a new instance of the class. + /// Name of column. + /// Set column as a key column. + public ColumnAttribute(string name, bool isKey) + : this(name) + { + IsKey = isKey; + } + + /// Initializes a new instance of the class. + /// Name of column. + /// Set column as a key column. + /// Set column type. + public ColumnAttribute(string name, bool isKey, DbType type) + : this(name, isKey) + { + Type = type; + } + + /// Initializes a new instance of the class. + /// Name of column. + /// Set column as a key column. + /// Set column type. + /// Set column value size. + public ColumnAttribute(string name, bool isKey, DbType type, int size) + : this(name, isKey, type) + { + Size = size; + } + + /// Initializes a new instance of the class. + /// Name of column. + /// Set column as a key column. + /// Set column type. + /// Set column value precision. + /// Set column value scale. + public ColumnAttribute(string name, bool isKey, DbType type, byte precision, byte scale) + : this(name, isKey, type) + { + Precision = precision; + Scale = scale; + } + + /// Initializes a new instance of the class. + /// Name of column. + /// Set column as a key column. + /// Set column type. + /// Set column value size. + /// Set column value precision. + /// Set column value scale. + public ColumnAttribute(string name, bool isKey, DbType type, int size, byte precision, byte scale) + : this(name, isKey, type, precision, scale) + { + Size = size; + } + + /// Initializes a new instance of the class. + /// Name of column. + /// Set column as a key column. + /// Set column has unique value. + /// Set column type. + /// Set column value size. + /// Set column value precision. + /// Set column value scale. + public ColumnAttribute(string name, bool isKey, bool isUnique, DbType type, int size, byte precision, byte scale) + : this(name, isKey, type, size, precision, scale) + { + IsUnique = isUnique; + } + + #endregion Constructors + } + + /// Class with mapper cache. + public static class DynamicMapperCache + { + private static readonly object SyncLock = new object(); + private static Dictionary _cache = new Dictionary(); + + /// Get type mapper. + /// Type of mapper. + /// Type mapper. + public static DynamicTypeMap GetMapper() + { + return GetMapper(typeof(T)); + } + + /// Get type mapper. + /// Type of mapper. + /// Type mapper. + public static DynamicTypeMap GetMapper(Type type) + { + if (type == null) + return null; + /*if (type.IsAnonymous()) + return null;*/ + + DynamicTypeMap mapper = null; + + lock (SyncLock) + { + if (!_cache.TryGetValue(type, out mapper)) + { + mapper = new DynamicTypeMap(type); + + if (mapper != null) + _cache.Add(type, mapper); + } + } + + return mapper; + } + } + + /// Dynamic property invoker. + public class DynamicPropertyInvoker + { + internal class ParameterSpec + { + public string Name { get; set; } + + public DbType Type { get; set; } + + public int Ordinal { get; set; } + } + + /// Gets the type of property. + public Type Type { get; private set; } + + /// Gets value getter. + public Func Get { get; private set; } + + /// Gets value setter. + public Action Set { get; private set; } + + /// Gets name of property. + public string Name { get; private set; } + + /// Gets type column description. + public ColumnAttribute Column { get; private set; } + + /// Gets a value indicating whether this is ignored in some cases. + public bool Ignore { get; private set; } + + /// Initializes a new instance of the class. + /// Property info to be invoked in the future. + /// Column attribute if exist. + public DynamicPropertyInvoker(PropertyInfo property, ColumnAttribute attr) + { + Name = property.Name; + Type = property.PropertyType; + + var ignore = property.GetCustomAttributes(typeof(IgnoreAttribute), false); + + Ignore = ignore != null && ignore.Length > 0; + + Column = attr; + + if (property.CanRead) + Get = CreateGetter(property); + + if (property.CanWrite) + Set = CreateSetter(property); + } + + private Func CreateGetter(PropertyInfo property) + { + if (!property.CanRead) + return null; + + var objParm = Expression.Parameter(typeof(object), "o"); + + return Expression.Lambda>( + Expression.Convert( + Expression.Property( + Expression.TypeAs(objParm, property.DeclaringType), + property.Name), + typeof(object)), objParm).Compile(); + } + + private Action CreateSetter(PropertyInfo property) + { + if (!property.CanWrite) + return null; + + var objParm = Expression.Parameter(typeof(object), "o"); + var valueParm = Expression.Parameter(typeof(object), "value"); + + return Expression.Lambda>( + Expression.Assign( + Expression.Property( + Expression.Convert(objParm, property.DeclaringType), + property.Name), + Expression.Convert(valueParm, property.PropertyType)), + objParm, valueParm).Compile(); + } + + #region Type command cache + + internal ParameterSpec InsertCommandParameter { get; set; } + + internal ParameterSpec UpdateCommandParameter { get; set; } + + internal ParameterSpec DeleteCommandParameter { get; set; } + + #endregion Type command cache + } + + /// Represents type columnMap. + public class DynamicTypeMap + { + /// Gets mapper destination type creator. + public Type Type { get; private set; } + + /// Gets type table description. + public TableAttribute Table { get; private set; } + + /// Gets object creator. + public Func Creator { get; private set; } + + /// Gets map of columns to properties. + /// Key: Column name (lower), Value: . + public Dictionary ColumnsMap { get; private set; } + + /// Gets map of properties to column. + /// Key: Property name, Value: Column name. + public Dictionary PropertyMap { get; private set; } + + /// Gets list of ignored properties. + public List Ignored { get; private set; } + + /// Initializes a new instance of the class. + /// Type to which columnMap objects. + public DynamicTypeMap(Type type) + { + Type = type; + + var attr = type.GetCustomAttributes(typeof(TableAttribute), false); + + if (attr != null && attr.Length > 0) + Table = (TableAttribute)attr[0]; + + Creator = CreateCreator(); + CreateColumnAndPropertyMap(); + } + + private void CreateColumnAndPropertyMap() + { + var columnMap = new Dictionary(); + var propertyMap = new Dictionary(); + var ignored = new List(); + + foreach (var pi in Type.GetProperties()) + { + ColumnAttribute attr = null; + + var attrs = pi.GetCustomAttributes(typeof(ColumnAttribute), true); + + if (attrs != null && attrs.Length > 0) + attr = (ColumnAttribute)attrs[0]; + + string col = attr == null || string.IsNullOrEmpty(attr.Name) ? pi.Name : attr.Name; + + var val = new DynamicPropertyInvoker(pi, attr); + columnMap.Add(col.ToLower(), val); + + propertyMap.Add(pi.Name, col); + + if (val.Ignore) + ignored.Add(pi.Name); + } + + ColumnsMap = columnMap; + PropertyMap = propertyMap; + + Ignored = ignored; ////columnMap.Where(i => i.Value.Ignore).Select(i => i.Value.Name).ToList(); + } + + private Func CreateCreator() + { + if (Type.GetConstructor(Type.EmptyTypes) != null) + return Expression.Lambda>(Expression.New(Type)).Compile(); + + return null; + } + + /// Create object of type and fill values from source. + /// Object containing values that will be mapped to newly created object. + /// New object of type with matching values from source. + public object Create(object source) + { + return Map(source, Creator()); + } + + /// Fill values from source to object in destination. + /// Object containing values that will be mapped to newly created object. + /// Object of type to which copy values from source. + /// Object of type with matching values from source. + public object Map(object source, object destination) + { + DynamicPropertyInvoker dpi = null; + + foreach (var item in source.ToDictionary()) + { + if (ColumnsMap.TryGetValue(item.Key.ToLower(), out dpi) && item.Value != null) + if (dpi.Set != null) + dpi.Set(destination, item.Value); + } + + return destination; + } + + #region Type command cache + + internal string InsertCommandText { get; set; } + + internal string UpdateCommandText { get; set; } + + internal string DeleteCommandText { get; set; } + + #endregion Type command cache + } + + /// Allows to add ignore action to property. + /// Property still get's mapped from output. + [AttributeUsage(AttributeTargets.Property)] + public class IgnoreAttribute : Attribute + { + } + + /// Allows to add table name to class. + [AttributeUsage(AttributeTargets.Class)] + public class TableAttribute : Attribute + { + /// Gets or sets table owner name. + public string Owner { get; set; } + + /// Gets or sets name. + public string Name { get; set; } + + /// Gets or sets a value indicating whether override database + /// schema values. + /// If database doesn't support schema, you still have to + /// set this to true to get schema from type. + public bool Override { get; set; } + } + } +} \ No newline at end of file diff --git a/AmalgamationTool/Program.cs b/AmalgamationTool/Program.cs new file mode 100644 index 0000000..0aaa7b1 --- /dev/null +++ b/AmalgamationTool/Program.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace AmalgamationTool +{ + internal class Program + { + private static void Main(string[] args) + { + List usings = new List(); + Dictionary> classes = new Dictionary>(); + + // Build a file using string builder. + StringBuilder sb = new StringBuilder(); + + foreach (var f in new DirectoryInfo(Path.GetFullPath(args[0].Trim('"', '\''))).GetFiles("*.cs", SearchOption.AllDirectories)) + { + string content = File.ReadAllText(f.FullName); + + string namespaceName = string.Empty; + + // Deal with usings + foreach (var u in content.Split(new string[] { Environment.NewLine }, StringSplitOptions.None) + .Where(l => l.TrimEnd().StartsWith("using ")) + .Select(l => l.Trim())) + if (!usings.Contains(u)) + usings.Add(u); + + // Extract namespace + var nstart = content.IndexOf("namespace ") + "namespace ".Length; + var bbrace = content.IndexOf("{", nstart); + var nlen = bbrace - nstart; + + if (nstart < "namespace ".Length) + { + if (f.Name.ToLower() == "assemblyinfo.cs") + { + var hs = content.IndexOf("/*"); + var es = content.IndexOf("*/", hs) + 2; + if (es > hs) + { + sb.AppendLine(content.Substring(hs, es - hs)); + sb.AppendLine(); + } + } + continue; + } + + string ns = content.Substring(nstart, nlen).Trim(); + + // Add namespace if not exist + if (!classes.ContainsKey(ns)) + classes.Add(ns, new List()); + + var ebrace = content.LastIndexOf('}'); + + // Cut content as class/enum + classes[ns].Add(content.Substring(bbrace + 1, ebrace - bbrace - 1)); + } + + usings.Sort(); + + foreach (var u in usings) + sb.AppendLine(u); + + sb.AppendLine(@" +[module: System.Diagnostics.CodeAnalysis.SuppressMessage(""StyleCop.CSharp.MaintainabilityRules"", ""SA1402:FileMayOnlyContainASingleClass"", Justification = ""This is a generated file which generates all the necessary support classes."")] +[module: System.Diagnostics.CodeAnalysis.SuppressMessage(""StyleCop.CSharp.MaintainabilityRules"", ""SA1403:FileMayOnlyContainASingleNamespace"", Justification = ""This is a generated file which generates all the necessary support classes."")]"); + + FillClassesAndNamespacesIddented(classes, sb); + + File.WriteAllText(Path.GetFullPath(args[1].Trim('"', '\'')), sb.ToString()); + } + + private static void FillClassesAndNamespaces(Dictionary> classes, StringBuilder sb) + { + foreach (var n in classes) + { + sb.AppendFormat("namespace {0}{1}{{", n.Key, Environment.NewLine); + n.Value.ForEach(c => sb.Append(c)); + sb.AppendLine("}"); + sb.AppendLine(string.Empty); + } + } + + private static void FillClassesAndNamespacesIddented(Dictionary> classes, StringBuilder sb) + { + foreach (var n in classes.Where(nc => nc.Key.Split('.').Count() == 1)) + { + sb.AppendFormat("namespace {0}{1}{{", n.Key, Environment.NewLine); + n.Value.ForEach(c => sb.Append(c)); + + SubNamespaces(classes, n.Key, sb, 1); + + sb.AppendLine("}"); + sb.AppendLine(string.Empty); + } + } + + private static void SubNamespaces(Dictionary> classes, string p, StringBuilder sb, int ident) + { + sb.AppendLine(string.Empty); + + foreach (var n in classes.Where(nc => nc.Key.Split('.').Count() == ident + 1 && nc.Key.StartsWith(p))) + { + for (int i = 0; i < ident; i++) sb.Append(" "); + sb.AppendFormat("namespace {0}{1}", n.Key.Substring(p.Length + 1), Environment.NewLine); + + for (int i = 0; i < ident; i++) sb.Append(" "); + sb.Append("{"); + n.Value.ForEach(c => + { + foreach (var l in c.Split(new string[] { Environment.NewLine }, StringSplitOptions.None)) + { + for (int i = 0; i < ident; i++) sb.Append(" "); + sb.AppendLine(l); + } + }); + + SubNamespaces(classes, n.Key, sb, ident + 1); + + for (int i = 0; i < ident; i++) sb.Append(" "); + sb.AppendLine("}"); + sb.AppendLine(string.Empty); + } + } + } +} \ No newline at end of file diff --git a/AmalgamationTool/Properties/AssemblyInfo.cs b/AmalgamationTool/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..6aa9b29 --- /dev/null +++ b/AmalgamationTool/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AmalgamationTool")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("AmalgamationTool")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("de540809-dd27-47b1-bb01-7dce3a880cde")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/DynamORM.Tests/Modify/ParserTests.cs b/DynamORM.Tests/Modify/ParserTests.cs index c650a9d..2c56c41 100644 --- a/DynamORM.Tests/Modify/ParserTests.cs +++ b/DynamORM.Tests/Modify/ParserTests.cs @@ -66,7 +66,7 @@ namespace DynamORM.Tests.Modify { IDynamicInsertQueryBuilder cmd = new DynamicInsertQueryBuilder(Database, "Users"); - cmd.Insert(x => x.Users.Code = "001", x => x.Users.Name = "Admin", x => x.Users.IsAdmin = 1); + cmd.Values(x => x.Users.Code = "001", x => x.Users.Name = "Admin", x => x.Users.IsAdmin = 1); Assert.AreEqual(string.Format(@"INSERT INTO ""Users"" (""Code"", ""Name"", ""IsAdmin"") VALUES ({0})", string.Join(", ", cmd.Parameters.Keys.Select(p => string.Format("[${0}]", p)))), cmd.CommandText()); @@ -80,7 +80,7 @@ namespace DynamORM.Tests.Modify { IDynamicInsertQueryBuilder cmd = new DynamicInsertQueryBuilder(Database, "Users"); - cmd.Insert(x => x.Code = "001", x => x.Name = "Admin", x => x.IsAdmin = x(cmd + cmd.Values(x => x.Code = "001", x => x.Name = "Admin", x => x.IsAdmin = x(cmd .SubQuery(a => a.AccessRights.As(a.a)) .Select(a => a.IsAdmin) .Where(a => a.User_Id == "001"))); @@ -97,7 +97,7 @@ namespace DynamORM.Tests.Modify { IDynamicInsertQueryBuilder cmd = new DynamicInsertQueryBuilder(Database, "Users"); - cmd.Insert(x => new { Code = "001", Name = "Admin", IsAdmin = 1 }); + cmd.Values(x => new { Code = "001", Name = "Admin", IsAdmin = 1 }); Assert.AreEqual(string.Format(@"INSERT INTO ""Users"" (""Code"", ""Name"", ""IsAdmin"") VALUES ({0})", string.Join(", ", cmd.Parameters.Keys.Select(p => string.Format("[${0}]", p)))), cmd.CommandText()); @@ -111,7 +111,7 @@ namespace DynamORM.Tests.Modify { IDynamicInsertQueryBuilder cmd = new DynamicInsertQueryBuilder(Database, "Users"); - cmd.Insert(x => new + cmd.Values(x => new { Code = "001", Name = "Admin", @@ -137,7 +137,7 @@ namespace DynamORM.Tests.Modify { IDynamicUpdateQueryBuilder cmd = new DynamicUpdateQueryBuilder(Database, "Users"); - cmd.Values(x => x.Users.Code = "001", x => x.Users.Name = "Admin", x => x.Users.IsAdmin = 1) + cmd.Set(x => x.Users.Code = "001", x => x.Users.Name = "Admin", x => x.Users.IsAdmin = 1) .Where(x => x.Users.Id_User == 1); Assert.AreEqual(string.Format(@"UPDATE ""Users"" SET ""Code"" = [${0}], ""Name"" = [${1}], ""IsAdmin"" = [${2}] WHERE (""Users"".""Id_User"" = [${3}])", @@ -152,7 +152,7 @@ namespace DynamORM.Tests.Modify { IDynamicUpdateQueryBuilder cmd = new DynamicUpdateQueryBuilder(Database, "Users"); - cmd.Values(x => x.Users.Code = "001", x => x.Users.Name = "Admin", x => x.Users.IsAdmin = x(cmd + cmd.Set(x => x.Users.Code = "001", x => x.Users.Name = "Admin", x => x.Users.IsAdmin = x(cmd .SubQuery(a => a.AccessRights.As(a.a)) .Select(a => a.IsAdmin) .Where(a => a.User_Id == a.Users.Id_User))) @@ -170,7 +170,7 @@ namespace DynamORM.Tests.Modify { IDynamicUpdateQueryBuilder cmd = new DynamicUpdateQueryBuilder(Database, "Users"); - cmd.Values(x => new { Code = "001", Name = "Admin", IsAdmin = 1 }) + cmd.Set(x => new { Code = "001", Name = "Admin", IsAdmin = 1 }) .Where(x => new { Id_User = 1 }); Assert.AreEqual(string.Format(@"UPDATE ""Users"" SET ""Code"" = [${0}], ""Name"" = [${1}], ""IsAdmin"" = [${2}] WHERE (""Id_User"" = [${3}])", @@ -185,7 +185,7 @@ namespace DynamORM.Tests.Modify { IDynamicUpdateQueryBuilder cmd = new DynamicUpdateQueryBuilder(Database, "Users"); - cmd.Values(x => new + cmd.Set(x => new { Code = "001", Name = "Admin", diff --git a/DynamORM.Tests/Select/DynamicAccessTests.cs b/DynamORM.Tests/Select/DynamicAccessTests.cs index adfb77b..9a9e18c 100644 --- a/DynamORM.Tests/Select/DynamicAccessTests.cs +++ b/DynamORM.Tests/Select/DynamicAccessTests.cs @@ -314,7 +314,7 @@ namespace DynamORM.Tests.Select Assert.AreEqual("Clarke,Marny,Dai,Forrest,Blossom,George,Ivory,Inez,Sigourney,Fulton,Logan,Anne,Alexandra,Adena,Lionel,Aimee,Selma,Lara,Ori", GetTestBuilder() .Where(x => x.id < 20) - .Select("group_concat(first):first") + .SelectColumn("group_concat(first):first") .Scalar()); } diff --git a/DynamORM.Tests/Select/LegacyParserTests.cs b/DynamORM.Tests/Select/LegacyParserTests.cs index a2f2961..cb1f38c 100644 --- a/DynamORM.Tests/Select/LegacyParserTests.cs +++ b/DynamORM.Tests/Select/LegacyParserTests.cs @@ -285,7 +285,7 @@ namespace DynamORM.Tests.Select IDynamicSelectQueryBuilder cmd = new DynamicSelectQueryBuilder(Database); cmd.From(x => x.dbo.Users.As(x.u)) - .OrderBy(new DynamicColumn("u.Name").Desc()); + .OrderByColumn(new DynamicColumn("u.Name").Desc()); Assert.AreEqual(string.Format("SELECT * FROM \"dbo\".\"Users\" AS u ORDER BY u.\"Name\" DESC"), cmd.CommandText()); } @@ -299,7 +299,7 @@ namespace DynamORM.Tests.Select IDynamicSelectQueryBuilder cmd = new DynamicSelectQueryBuilder(Database); cmd.From(x => x.dbo.Users.As(x.u)) - .OrderBy(new DynamicColumn("u.Name").SetAlias("1").Desc()); + .OrderByColumn(new DynamicColumn("u.Name").SetAlias("1").Desc()); Assert.AreEqual(string.Format("SELECT * FROM \"dbo\".\"Users\" AS u ORDER BY 1 DESC"), cmd.CommandText()); } @@ -313,7 +313,7 @@ namespace DynamORM.Tests.Select IDynamicSelectQueryBuilder cmd = new DynamicSelectQueryBuilder(Database); cmd.From(x => x.dbo.Users.As(x.u)) - .GroupBy(new DynamicColumn("u.Name")); + .GroupByColumn(new DynamicColumn("u.Name")); Assert.AreEqual(string.Format("SELECT * FROM \"dbo\".\"Users\" AS u GROUP BY u.\"Name\""), cmd.CommandText()); } diff --git a/DynamORM.Tests/Select/TypedAccessTests.cs b/DynamORM.Tests/Select/TypedAccessTests.cs index 1fcfb1b..c054d55 100644 --- a/DynamORM.Tests/Select/TypedAccessTests.cs +++ b/DynamORM.Tests/Select/TypedAccessTests.cs @@ -357,7 +357,7 @@ namespace DynamORM.Tests.Select GetTestBuilder() .From(x => x(typeof(T)).As(x.t)) .Where(x => x.t.id < 20) - .Select("group_concat(first):first") + .SelectColumn("group_concat(first):first") .Scalar()); } diff --git a/DynamORM/Builders/IDynamicInsertQueryBuilder.cs b/DynamORM/Builders/IDynamicInsertQueryBuilder.cs index c0b3c9a..0bf7c07 100644 --- a/DynamORM/Builders/IDynamicInsertQueryBuilder.cs +++ b/DynamORM/Builders/IDynamicInsertQueryBuilder.cs @@ -44,9 +44,10 @@ namespace DynamORM.Builders /// - Resolve to a string, in this case a '=' must appear in the string. /// - Resolve to a expression with the form: 'x => x.Column = Value'. /// + /// The specifications. /// The specifications. /// This instance to permit chaining. - IDynamicInsertQueryBuilder Insert(params Func[] func); + IDynamicInsertQueryBuilder Values(Func fn, params Func[] func); /// Add insert fields. /// Insert column. @@ -56,9 +57,7 @@ namespace DynamORM.Builders /// Add insert fields. /// Set insert value as properties and values of an object. - /// If true use schema to determine key columns and ignore those which - /// aren't keys. /// Builder instance. - IDynamicInsertQueryBuilder Insert(object o, bool schema = false); + IDynamicInsertQueryBuilder Insert(object o); } } \ No newline at end of file diff --git a/DynamORM/Builders/IDynamicSelectQueryBuilder.cs b/DynamORM/Builders/IDynamicSelectQueryBuilder.cs index eb5ac98..3381c3b 100644 --- a/DynamORM/Builders/IDynamicSelectQueryBuilder.cs +++ b/DynamORM/Builders/IDynamicSelectQueryBuilder.cs @@ -63,9 +63,10 @@ namespace DynamORM.Builders /// - Generic expression: 'x => x( expression ).As( x.Alias )', where the alias part is mandatory. In this /// case the alias is not annotated. /// + /// The specification. /// The specification. /// This instance to permit chaining. - IDynamicSelectQueryBuilder From(params Func[] func); + IDynamicSelectQueryBuilder From(Func fn, params Func[] func); /// /// Adds to the 'Join' clause the contents obtained by parsing the dynamic lambda expressions given. The supported @@ -139,21 +140,22 @@ namespace DynamORM.Builders /// - Generic expression: 'x => x( expression ).As( x.Alias )', where the alias part is mandatory. In this case /// the alias is not annotated. /// + /// The specification. /// The specification. /// This instance to permit chaining. - IDynamicSelectQueryBuilder Select(params Func[] func); + IDynamicSelectQueryBuilder Select(Func fn, params Func[] func); /// Add select columns. /// Columns to add to object. /// Builder instance. - IDynamicSelectQueryBuilder Select(params DynamicColumn[] columns); + IDynamicSelectQueryBuilder SelectColumn(params DynamicColumn[] columns); /// Add select columns. /// Columns to add to object. /// Column format consist of Column Name, Alias and /// Aggregate function in this order separated by ':'. /// Builder instance. - IDynamicSelectQueryBuilder Select(params string[] columns); + IDynamicSelectQueryBuilder SelectColumn(params string[] columns); #endregion Select @@ -162,21 +164,22 @@ namespace DynamORM.Builders /// /// Adds to the 'Group By' clause the contents obtained from from parsing the dynamic lambda expression given. /// + /// The specification. /// The specification. /// This instance to permit chaining. - IDynamicSelectQueryBuilder GroupBy(params Func[] func); + IDynamicSelectQueryBuilder GroupBy(Func fn, params Func[] func); /// Add select columns. /// Columns to group by. /// Builder instance. - IDynamicSelectQueryBuilder GroupBy(params DynamicColumn[] columns); + IDynamicSelectQueryBuilder GroupByColumn(params DynamicColumn[] columns); /// Add select columns. /// Columns to group by. /// Column format consist of Column Name and /// Alias in this order separated by ':'. /// Builder instance. - IDynamicSelectQueryBuilder GroupBy(params string[] columns); + IDynamicSelectQueryBuilder GroupByColumn(params string[] columns); #endregion GroupBy @@ -188,21 +191,22 @@ namespace DynamORM.Builders /// to specify the direction. If no virtual method is used, the default is ascending order. You can also use the /// shorter versions Asc() and Desc(). /// + /// The specification. /// The specification. /// This instance to permit chaining. - IDynamicSelectQueryBuilder OrderBy(params Func[] func); + IDynamicSelectQueryBuilder OrderBy(Func fn, params Func[] func); /// Add select columns. /// Columns to order by. /// Builder instance. - IDynamicSelectQueryBuilder OrderBy(params DynamicColumn[] columns); + IDynamicSelectQueryBuilder OrderByColumn(params DynamicColumn[] columns); /// Add select columns. /// Columns to order by. /// Column format consist of Column Name and /// Alias in this order separated by ':'. /// Builder instance. - IDynamicSelectQueryBuilder OrderBy(params string[] columns); + IDynamicSelectQueryBuilder OrderByColumn(params string[] columns); #endregion OrderBy diff --git a/DynamORM/Builders/IDynamicUpdateQueryBuilder.cs b/DynamORM/Builders/IDynamicUpdateQueryBuilder.cs index fb20700..c317454 100644 --- a/DynamORM/Builders/IDynamicUpdateQueryBuilder.cs +++ b/DynamORM/Builders/IDynamicUpdateQueryBuilder.cs @@ -63,7 +63,7 @@ namespace DynamORM.Builders /// /// The specifications. /// This instance to permit chaining. - IDynamicUpdateQueryBuilder Values(params Func[] func); + IDynamicUpdateQueryBuilder Set(params Func[] func); /// Add insert fields. /// Insert column. diff --git a/DynamORM/Builders/Implementation/DynamicInsertQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicInsertQueryBuilder.cs index fc517df..fba09cf 100644 --- a/DynamORM/Builders/Implementation/DynamicInsertQueryBuilder.cs +++ b/DynamORM/Builders/Implementation/DynamicInsertQueryBuilder.cs @@ -80,62 +80,67 @@ namespace DynamORM.Builders.Implementation /// - Resolve to a string, in this case a '=' must appear in the string. /// - Resolve to a expression with the form: 'x => x.Column = Value'. /// + /// The specifications. /// The specifications. /// This instance to permit chaining. - public virtual IDynamicInsertQueryBuilder Insert(params Func[] func) + public virtual IDynamicInsertQueryBuilder Values(Func fn, params Func[] func) { - if (func == null) + if (fn == null) throw new ArgumentNullException("Array of specifications cannot be null."); - int index = -1; + int index = InsertFunc(-1, fn); - foreach (var f in func) - { - index++; - - if (f == null) - throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); - - using (var parser = DynamicParser.Parse(f)) - { - var result = parser.Result; - if (result == null) - throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); - - string main = null; - string value = null; - string str = null; - - // When 'x => x.Table.Column = value' or 'x => x.Column = value'... - if (result is DynamicParser.Node.SetMember) - { - var node = (DynamicParser.Node.SetMember)result; - - DynamicSchemaColumn? col = GetColumnFromSchema(node.Name); - main = Database.DecorateName(node.Name); - value = Parse(node.Value, pars: Parameters, nulls: true, columnSchema: col); - - _columns = _columns == null ? main : string.Format("{0}, {1}", _columns, main); - _values = _values == null ? value : string.Format("{0}, {1}", _values, value); - continue; - } - else if (!(result is DynamicParser.Node) && !result.GetType().IsValueType) - { - Insert(result); - continue; - } - - // Other specifications are considered invalid... - var err = string.Format("Specification '{0}' is invalid.", result); - str = Parse(result); - if (str.Contains("=")) err += " May have you used a '==' instead of a '=' operator?"; - throw new ArgumentException(err); - } - } + if (func != null) + foreach (var f in func) + index = InsertFunc(index, f); return this; } + private int InsertFunc(int index, Func f) + { + index++; + + if (f == null) + throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); + + using (var parser = DynamicParser.Parse(f)) + { + var result = parser.Result; + if (result == null) + throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); + + string main = null; + string value = null; + string str = null; + + // When 'x => x.Table.Column = value' or 'x => x.Column = value'... + if (result is DynamicParser.Node.SetMember) + { + var node = (DynamicParser.Node.SetMember)result; + + DynamicSchemaColumn? col = GetColumnFromSchema(node.Name); + main = Database.DecorateName(node.Name); + value = Parse(node.Value, pars: Parameters, nulls: true, columnSchema: col); + + _columns = _columns == null ? main : string.Format("{0}, {1}", _columns, main); + _values = _values == null ? value : string.Format("{0}, {1}", _values, value); + return index; + } + else if (!(result is DynamicParser.Node) && !result.GetType().IsValueType) + { + Insert(result); + return index; + } + + // Other specifications are considered invalid... + var err = string.Format("Specification '{0}' is invalid.", result); + str = Parse(result); + if (str.Contains("=")) err += " May have you used a '==' instead of a '=' operator?"; + throw new ArgumentException(err); + } + } + /// Add insert fields. /// Insert column. /// Insert value. @@ -161,10 +166,8 @@ namespace DynamORM.Builders.Implementation /// Add insert fields. /// Set insert value as properties and values of an object. - /// If true use schema to determine key columns and ignore those which - /// aren't keys. /// Builder instance. - public virtual IDynamicInsertQueryBuilder Insert(object o, bool schema = false) + public virtual IDynamicInsertQueryBuilder Insert(object o) { if (o is DynamicColumn) { diff --git a/DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs index 3c90525..c9e4e78 100644 --- a/DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs +++ b/DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs @@ -251,176 +251,184 @@ namespace DynamORM.Builders.Implementation /// - Generic expression: 'x => x( expression ).As( x.Alias )', where the alias part is mandatory. In this /// case the alias is not annotated. /// + /// The specification. /// The specification. /// This instance to permit chaining. - public virtual IDynamicSelectQueryBuilder From(params Func[] func) + public virtual IDynamicSelectQueryBuilder From(Func fn, params Func[] func) { - if (func == null) - throw new ArgumentNullException("Array of functions cannot be null."); + if (fn == null) + throw new ArgumentNullException("Array of functions cannot be or contain null."); - int index = -1; + int index = FromFunc(-1, fn); foreach (var f in func) - { - index++; - ITableInfo tableInfo = null; - using (var parser = DynamicParser.Parse(f)) - { - var result = parser.Result; - - // If the expression result is string. - if (result is string) - { - var node = (string)result; - var tuple = node.SplitSomethingAndAlias(); - var parts = tuple.Item1.Split('.'); - tableInfo = new TableInfo(Database, - Database.StripName(parts.Last()).Validated("Table"), - tuple.Item2.Validated("Alias", canbeNull: true), - parts.Length == 2 ? Database.StripName(parts.First()).Validated("Owner", canbeNull: true) : null); - } - else if (result is Type) - { - Type type = (Type)result; - if (type.IsAnonymous()) - throw new InvalidOperationException(string.Format("Cant assign anonymous type as a table ({0}). Parsing {1}", type.FullName, result)); - - var mapper = DynamicMapperCache.GetMapper(type); - - if (mapper == null) - throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}). Parsing {1}", type.FullName, result)); - - tableInfo = new TableInfo(Database, type); - } - else if (result is DynamicParser.Node) - { - // Or if it resolves to a dynamic node - var node = (DynamicParser.Node)result; - - string owner = null; - string main = null; - string alias = null; - Type type = null; - - while (true) - { - // Support for the AS() virtual method... - if (node is DynamicParser.Node.Method && ((DynamicParser.Node.Method)node).Name.ToUpper() == "AS") - { - if (alias != null) - throw new ArgumentException(string.Format("Alias '{0}' is already set when parsing '{1}'.", alias, result)); - - object[] args = ((DynamicParser.Node.Method)node).Arguments; - - if (args == null) - throw new ArgumentNullException("arg", "AS() is not a parameterless method."); - - if (args.Length != 1) - throw new ArgumentException("AS() requires one and only one parameter: " + args.Sketch()); - - alias = Parse(args[0], rawstr: true, decorate: false).Validated("Alias"); - - node = node.Host; - continue; - } - - /*if (node is DynamicParser.Node.Method && ((DynamicParser.Node.Method)node).Name.ToUpper() == "subquery") - { - main = Parse(this.SubQuery(((DynamicParser.Node.Method)node).Arguments.Where(p => p is Func).Cast>().ToArray()), Parameters); - continue; - }*/ - - // Support for table specifications... - if (node is DynamicParser.Node.GetMember) - { - if (owner != null) - throw new ArgumentException(string.Format("Owner '{0}.{1}' is already set when parsing '{2}'.", owner, main, result)); - - if (main != null) - owner = ((DynamicParser.Node.GetMember)node).Name; - else - main = ((DynamicParser.Node.GetMember)node).Name; - - node = node.Host; - continue; - } - - // Support for generic sources... - if (node is DynamicParser.Node.Invoke) - { - if (owner != null) - throw new ArgumentException(string.Format("Owner '{0}.{1}' is already set when parsing '{2}'.", owner, main, result)); - - if (main != null) - owner = string.Format("{0}", Parse(node, rawstr: true, pars: Parameters)); - else - { - var invoke = (DynamicParser.Node.Invoke)node; - if (invoke.Arguments.Length == 1 && invoke.Arguments[0] is Type) - { - type = (Type)invoke.Arguments[0]; - if (type.IsAnonymous()) - throw new InvalidOperationException(string.Format("Cant assign anonymous type as a table ({0}). Parsing {1}", type.FullName, result)); - - var mapper = DynamicMapperCache.GetMapper(type); - - if (mapper == null) - throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}). Parsing {1}", type.FullName, result)); - - main = mapper.Table == null || string.IsNullOrEmpty(mapper.Table.Name) ? - mapper.Type.Name : mapper.Table.Name; - - owner = (mapper.Table != null) ? mapper.Table.Owner : owner; - } - else - main = string.Format("{0}", Parse(node, rawstr: true, pars: Parameters)); - } - - node = node.Host; - continue; - } - - // Just finished the parsing... - if (node is DynamicParser.Node.Argument) break; - - // All others are assumed to be part of the main element... - if (main != null) - main = Parse(node, pars: Parameters); - else - main = Parse(node, pars: Parameters); - - break; - } - - if (!string.IsNullOrEmpty(main)) - tableInfo = type == null ? new TableInfo(Database, main, alias, owner) : new TableInfo(Database, type, alias, owner); - else - throw new ArgumentException(string.Format("Specification #{0} is invalid: {1}", index, result)); - } - - // Or it is a not supported expression... - if (tableInfo == null) - throw new ArgumentException(string.Format("Specification #{0} is invalid: {1}", index, result)); - - Tables.Add(tableInfo); - - // We finally add the contents... - StringBuilder sb = new StringBuilder(); - - if (!string.IsNullOrEmpty(tableInfo.Owner)) - sb.AppendFormat("{0}.", Database.DecorateName(tableInfo.Owner)); - - sb.Append(tableInfo.Name.ContainsAny(StringExtensions.InvalidMemberChars) ? tableInfo.Name : Database.DecorateName(tableInfo.Name)); - - if (!string.IsNullOrEmpty(tableInfo.Alias)) - sb.AppendFormat(" AS {0}", tableInfo.Alias); - - _from = string.IsNullOrEmpty(_from) ? sb.ToString() : string.Format("{0}, {1}", _from, sb.ToString()); - } - } + index = FromFunc(index, f); return this; } + private int FromFunc(int index, Func f) + { + if (f == null) + throw new ArgumentNullException("Array of functions cannot be or contain null."); + + index++; + ITableInfo tableInfo = null; + using (var parser = DynamicParser.Parse(f)) + { + var result = parser.Result; + + // If the expression result is string. + if (result is string) + { + var node = (string)result; + var tuple = node.SplitSomethingAndAlias(); + var parts = tuple.Item1.Split('.'); + tableInfo = new TableInfo(Database, + Database.StripName(parts.Last()).Validated("Table"), + tuple.Item2.Validated("Alias", canbeNull: true), + parts.Length == 2 ? Database.StripName(parts.First()).Validated("Owner", canbeNull: true) : null); + } + else if (result is Type) + { + Type type = (Type)result; + if (type.IsAnonymous()) + throw new InvalidOperationException(string.Format("Cant assign anonymous type as a table ({0}). Parsing {1}", type.FullName, result)); + + var mapper = DynamicMapperCache.GetMapper(type); + + if (mapper == null) + throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}). Parsing {1}", type.FullName, result)); + + tableInfo = new TableInfo(Database, type); + } + else if (result is DynamicParser.Node) + { + // Or if it resolves to a dynamic node + var node = (DynamicParser.Node)result; + + string owner = null; + string main = null; + string alias = null; + Type type = null; + + while (true) + { + // Support for the AS() virtual method... + if (node is DynamicParser.Node.Method && ((DynamicParser.Node.Method)node).Name.ToUpper() == "AS") + { + if (alias != null) + throw new ArgumentException(string.Format("Alias '{0}' is already set when parsing '{1}'.", alias, result)); + + object[] args = ((DynamicParser.Node.Method)node).Arguments; + + if (args == null) + throw new ArgumentNullException("arg", "AS() is not a parameterless method."); + + if (args.Length != 1) + throw new ArgumentException("AS() requires one and only one parameter: " + args.Sketch()); + + alias = Parse(args[0], rawstr: true, decorate: false).Validated("Alias"); + + node = node.Host; + continue; + } + + /*if (node is DynamicParser.Node.Method && ((DynamicParser.Node.Method)node).Name.ToUpper() == "subquery") + { + main = Parse(this.SubQuery(((DynamicParser.Node.Method)node).Arguments.Where(p => p is Func).Cast>().ToArray()), Parameters); + continue; + }*/ + + // Support for table specifications... + if (node is DynamicParser.Node.GetMember) + { + if (owner != null) + throw new ArgumentException(string.Format("Owner '{0}.{1}' is already set when parsing '{2}'.", owner, main, result)); + + if (main != null) + owner = ((DynamicParser.Node.GetMember)node).Name; + else + main = ((DynamicParser.Node.GetMember)node).Name; + + node = node.Host; + continue; + } + + // Support for generic sources... + if (node is DynamicParser.Node.Invoke) + { + if (owner != null) + throw new ArgumentException(string.Format("Owner '{0}.{1}' is already set when parsing '{2}'.", owner, main, result)); + + if (main != null) + owner = string.Format("{0}", Parse(node, rawstr: true, pars: Parameters)); + else + { + var invoke = (DynamicParser.Node.Invoke)node; + if (invoke.Arguments.Length == 1 && invoke.Arguments[0] is Type) + { + type = (Type)invoke.Arguments[0]; + if (type.IsAnonymous()) + throw new InvalidOperationException(string.Format("Cant assign anonymous type as a table ({0}). Parsing {1}", type.FullName, result)); + + var mapper = DynamicMapperCache.GetMapper(type); + + if (mapper == null) + throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}). Parsing {1}", type.FullName, result)); + + main = mapper.Table == null || string.IsNullOrEmpty(mapper.Table.Name) ? + mapper.Type.Name : mapper.Table.Name; + + owner = (mapper.Table != null) ? mapper.Table.Owner : owner; + } + else + main = string.Format("{0}", Parse(node, rawstr: true, pars: Parameters)); + } + + node = node.Host; + continue; + } + + // Just finished the parsing... + if (node is DynamicParser.Node.Argument) break; + + // All others are assumed to be part of the main element... + if (main != null) + main = Parse(node, pars: Parameters); + else + main = Parse(node, pars: Parameters); + + break; + } + + if (!string.IsNullOrEmpty(main)) + tableInfo = type == null ? new TableInfo(Database, main, alias, owner) : new TableInfo(Database, type, alias, owner); + else + throw new ArgumentException(string.Format("Specification #{0} is invalid: {1}", index, result)); + } + + // Or it is a not supported expression... + if (tableInfo == null) + throw new ArgumentException(string.Format("Specification #{0} is invalid: {1}", index, result)); + + Tables.Add(tableInfo); + + // We finally add the contents... + StringBuilder sb = new StringBuilder(); + + if (!string.IsNullOrEmpty(tableInfo.Owner)) + sb.AppendFormat("{0}.", Database.DecorateName(tableInfo.Owner)); + + sb.Append(tableInfo.Name.ContainsAny(StringExtensions.InvalidMemberChars) ? tableInfo.Name : Database.DecorateName(tableInfo.Name)); + + if (!string.IsNullOrEmpty(tableInfo.Alias)) + sb.AppendFormat(" AS {0}", tableInfo.Alias); + + _from = string.IsNullOrEmpty(_from) ? sb.ToString() : string.Format("{0}, {1}", _from, sb.ToString()); + } + return index; + } + /// /// Adds to the 'Join' clause the contents obtained by parsing the dynamic lambda expressions given. The supported /// formats are: @@ -780,94 +788,100 @@ namespace DynamORM.Builders.Implementation /// - Generic expression: 'x => x( expression ).As( x.Alias )', where the alias part is mandatory. In this case /// the alias is not annotated. /// + /// The specification. /// The specification. /// This instance to permit chaining. - public virtual IDynamicSelectQueryBuilder Select(params Func[] func) + public virtual IDynamicSelectQueryBuilder Select(Func fn, params Func[] func) { - if (func == null) + if (fn == null) throw new ArgumentNullException("Array of specifications cannot be null."); - int index = -1; - foreach (var f in func) - { - index++; - if (f == null) - throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); - - using (var parser = DynamicParser.Parse(f)) - { - var result = parser.Result; - if (result == null) - throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); - - string main = null; - string alias = null; - bool all = false; - bool anon = false; - - // If the expression resolves to a string... - if (result is string) - { - var node = (string)result; - var tuple = node.SplitSomethingAndAlias(); - main = tuple.Item1.Validated("Table and/or Column"); - - main = FixObjectName(main); - - alias = tuple.Item2.Validated("Alias", canbeNull: true); - } - else if (result is DynamicParser.Node) - { - // Or if it resolves to a dynamic node... - ParseSelectNode(result, ref main, ref alias, ref all); - } - else if (result.GetType().IsAnonymous()) - { - anon = true; - - foreach (var prop in result.ToDictionary()) - { - if (prop.Value is string) - { - var node = (string)prop.Value; - var tuple = node.SplitSomethingAndAlias(); - main = FixObjectName(tuple.Item1.Validated("Table and/or Column")); - - ////alias = tuple.Item2.Validated("Alias", canbeNull: true); - } - else if (prop.Value is DynamicParser.Node) - { - // Or if it resolves to a dynamic node... - ParseSelectNode(prop.Value, ref main, ref alias, ref all); - } - else - { - // Or it is a not supported expression... - throw new ArgumentException(string.Format("Specification #{0} in anonymous type is invalid: {1}", index, prop.Value)); - } - - alias = Database.DecorateName(prop.Key); - ParseSelectAddColumn(main, alias, all); - } - } - else - { - // Or it is a not supported expression... - throw new ArgumentException(string.Format("Specification #{0} is invalid: {1}", index, result)); - } - - if (!anon) - ParseSelectAddColumn(main, alias, all); - } - } + int index = SelectFunc(-1, fn); + if (func != null) + foreach (var f in func) + index = SelectFunc(index, f); return this; } + private int SelectFunc(int index, Func f) + { + index++; + if (f == null) + throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); + + using (var parser = DynamicParser.Parse(f)) + { + var result = parser.Result; + if (result == null) + throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); + + string main = null; + string alias = null; + bool all = false; + bool anon = false; + + // If the expression resolves to a string... + if (result is string) + { + var node = (string)result; + var tuple = node.SplitSomethingAndAlias(); + main = tuple.Item1.Validated("Table and/or Column"); + + main = FixObjectName(main); + + alias = tuple.Item2.Validated("Alias", canbeNull: true); + } + else if (result is DynamicParser.Node) + { + // Or if it resolves to a dynamic node... + ParseSelectNode(result, ref main, ref alias, ref all); + } + else if (result.GetType().IsAnonymous()) + { + anon = true; + + foreach (var prop in result.ToDictionary()) + { + if (prop.Value is string) + { + var node = (string)prop.Value; + var tuple = node.SplitSomethingAndAlias(); + main = FixObjectName(tuple.Item1.Validated("Table and/or Column")); + + ////alias = tuple.Item2.Validated("Alias", canbeNull: true); + } + else if (prop.Value is DynamicParser.Node) + { + // Or if it resolves to a dynamic node... + ParseSelectNode(prop.Value, ref main, ref alias, ref all); + } + else + { + // Or it is a not supported expression... + throw new ArgumentException(string.Format("Specification #{0} in anonymous type is invalid: {1}", index, prop.Value)); + } + + alias = Database.DecorateName(prop.Key); + ParseSelectAddColumn(main, alias, all); + } + } + else + { + // Or it is a not supported expression... + throw new ArgumentException(string.Format("Specification #{0} is invalid: {1}", index, result)); + } + + if (!anon) + ParseSelectAddColumn(main, alias, all); + } + return index; + } + /// Add select columns. /// Columns to add to object. /// Builder instance. - public virtual IDynamicSelectQueryBuilder Select(params DynamicColumn[] columns) + public virtual IDynamicSelectQueryBuilder SelectColumn(params DynamicColumn[] columns) { foreach (var col in columns) Select(x => col.ToSQLSelectColumn(Database)); @@ -880,9 +894,9 @@ namespace DynamORM.Builders.Implementation /// Column format consist of Column Name, Alias and /// Aggregate function in this order separated by ':'. /// Builder instance. - public virtual IDynamicSelectQueryBuilder Select(params string[] columns) + public virtual IDynamicSelectQueryBuilder SelectColumn(params string[] columns) { - return Select(columns.Select(c => DynamicColumn.ParseSelectColumn(c)).ToArray()); + return SelectColumn(columns.Select(c => DynamicColumn.ParseSelectColumn(c)).ToArray()); } #endregion Select @@ -892,48 +906,54 @@ namespace DynamORM.Builders.Implementation /// /// Adds to the 'Group By' clause the contents obtained from from parsing the dynamic lambda expression given. /// + /// The specification. /// The specification. /// This instance to permit chaining. - public virtual IDynamicSelectQueryBuilder GroupBy(params Func[] func) + public virtual IDynamicSelectQueryBuilder GroupBy(Func fn, params Func[] func) { - if (func == null) + if (fn == null) throw new ArgumentNullException("Array of specifications cannot be null."); - int index = -1; + int index = GroupByFunc(-1, fn); - foreach (var f in func) - { - index++; - if (f == null) - throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); - using (var parser = DynamicParser.Parse(f)) - { - var result = parser.Result; - if (result == null) - throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); - - string main = null; - - if (result is string) - main = FixObjectName(result as string); - else - main = Parse(result, pars: Parameters); - - main = main.Validated("Group By"); - if (_groupby == null) - _groupby = main; - else - _groupby = string.Format("{0}, {1}", _groupby, main); - } - } + if (func != null) + foreach (var f in func) + index = GroupByFunc(index, f); return this; } + private int GroupByFunc(int index, Func f) + { + index++; + if (f == null) + throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); + using (var parser = DynamicParser.Parse(f)) + { + var result = parser.Result; + if (result == null) + throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); + + string main = null; + + if (result is string) + main = FixObjectName(result as string); + else + main = Parse(result, pars: Parameters); + + main = main.Validated("Group By"); + if (_groupby == null) + _groupby = main; + else + _groupby = string.Format("{0}, {1}", _groupby, main); + } + return index; + } + /// Add select columns. /// Columns to group by. /// Builder instance. - public virtual IDynamicSelectQueryBuilder GroupBy(params DynamicColumn[] columns) + public virtual IDynamicSelectQueryBuilder GroupByColumn(params DynamicColumn[] columns) { foreach (var col in columns) GroupBy(x => col.ToSQLGroupByColumn(Database)); @@ -946,9 +966,9 @@ namespace DynamORM.Builders.Implementation /// Column format consist of Column Name and /// Alias in this order separated by ':'. /// Builder instance. - public virtual IDynamicSelectQueryBuilder GroupBy(params string[] columns) + public virtual IDynamicSelectQueryBuilder GroupByColumn(params string[] columns) { - return GroupBy(columns.Select(c => DynamicColumn.ParseSelectColumn(c)).ToArray()); + return GroupByColumn(columns.Select(c => DynamicColumn.ParseSelectColumn(c)).ToArray()); } #endregion GroupBy @@ -961,101 +981,107 @@ namespace DynamORM.Builders.Implementation /// to specify the direction. If no virtual method is used, the default is ascending order. You can also use the /// shorter versions Asc() and Desc(). /// + /// The specification. /// The specification. /// This instance to permit chaining. - public virtual IDynamicSelectQueryBuilder OrderBy(params Func[] func) + public virtual IDynamicSelectQueryBuilder OrderBy(Func fn, params Func[] func) { - if (func == null) + if (fn == null) throw new ArgumentNullException("Array of specifications cannot be null."); - int index = -1; + int index = OrderByFunc(-1, fn); - foreach (var f in func) - { - index++; - if (f == null) - throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); - - using (var parser = DynamicParser.Parse(f)) - { - var result = parser.Result; - if (result == null) throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); - - string main = null; - bool ascending = true; - - if (result is int) - main = result.ToString(); - else if (result is string) - { - var parts = ((string)result).Split(' '); - main = Database.StripName(parts.First()); - - int colNo; - if (!Int32.TryParse(main, out colNo)) - main = FixObjectName(main); - - ascending = parts.Length != 2 || parts.Last().ToUpper() == "ASCENDING" || parts.Last().ToUpper() == "ASC"; - } - else - { - // Intercepting trailing 'Ascending' or 'Descending' virtual methods... - if (result is DynamicParser.Node.Method) - { - var node = (DynamicParser.Node.Method)result; - var name = node.Name.ToUpper(); - if (name == "ASCENDING" || name == "ASC" || name == "DESCENDING" || name == "DESC") - { - object[] args = node.Arguments; - if (args != null && !(node.Host is DynamicParser.Node.Argument)) - throw new ArgumentException(string.Format("{0} must be a parameterless method, but found: {1}.", name, args.Sketch())); - else if ((args == null || args.Length != 1) && node.Host is DynamicParser.Node.Argument) - throw new ArgumentException(string.Format("{0} requires one numeric parameter, but found: {1}.", name, args.Sketch())); - - ascending = (name == "ASCENDING" || name == "ASC") ? true : false; - - if (args != null && args.Length == 1) - { - int col = -1; - if (args[0] is int) - main = args[0].ToString(); - else if (args[0] is string) - { - if (Int32.TryParse(args[0].ToString(), out col)) - main = col.ToString(); - else - main = FixObjectName(args[0].ToString()); - } - else - main = Parse(args[0], pars: Parameters); - } - - result = node.Host; - } - } - - // Just parsing the contents... - if (!(result is DynamicParser.Node.Argument)) - main = Parse(result, pars: Parameters); - } - - main = main.Validated("Order By"); - main = string.Format("{0} {1}", main, ascending ? "ASC" : "DESC"); - - if (_orderby == null) - _orderby = main; - else - _orderby = string.Format("{0}, {1}", _orderby, main); - } - } + if (func != null) + foreach (var f in func) + index = OrderByFunc(index, f); return this; } + private int OrderByFunc(int index, Func f) + { + index++; + if (f == null) + throw new ArgumentNullException(string.Format("Specification #{0} cannot be null.", index)); + + using (var parser = DynamicParser.Parse(f)) + { + var result = parser.Result; + if (result == null) throw new ArgumentException(string.Format("Specification #{0} resolves to null.", index)); + + string main = null; + bool ascending = true; + + if (result is int) + main = result.ToString(); + else if (result is string) + { + var parts = ((string)result).Split(' '); + main = Database.StripName(parts.First()); + + int colNo; + if (!Int32.TryParse(main, out colNo)) + main = FixObjectName(main); + + ascending = parts.Length != 2 || parts.Last().ToUpper() == "ASCENDING" || parts.Last().ToUpper() == "ASC"; + } + else + { + // Intercepting trailing 'Ascending' or 'Descending' virtual methods... + if (result is DynamicParser.Node.Method) + { + var node = (DynamicParser.Node.Method)result; + var name = node.Name.ToUpper(); + if (name == "ASCENDING" || name == "ASC" || name == "DESCENDING" || name == "DESC") + { + object[] args = node.Arguments; + if (args != null && !(node.Host is DynamicParser.Node.Argument)) + throw new ArgumentException(string.Format("{0} must be a parameterless method, but found: {1}.", name, args.Sketch())); + else if ((args == null || args.Length != 1) && node.Host is DynamicParser.Node.Argument) + throw new ArgumentException(string.Format("{0} requires one numeric parameter, but found: {1}.", name, args.Sketch())); + + ascending = (name == "ASCENDING" || name == "ASC") ? true : false; + + if (args != null && args.Length == 1) + { + int col = -1; + if (args[0] is int) + main = args[0].ToString(); + else if (args[0] is string) + { + if (Int32.TryParse(args[0].ToString(), out col)) + main = col.ToString(); + else + main = FixObjectName(args[0].ToString()); + } + else + main = Parse(args[0], pars: Parameters); + } + + result = node.Host; + } + } + + // Just parsing the contents... + if (!(result is DynamicParser.Node.Argument)) + main = Parse(result, pars: Parameters); + } + + main = main.Validated("Order By"); + main = string.Format("{0} {1}", main, ascending ? "ASC" : "DESC"); + + if (_orderby == null) + _orderby = main; + else + _orderby = string.Format("{0}, {1}", _orderby, main); + } + return index; + } + /// Add select columns. /// Columns to order by. /// Builder instance. - public virtual IDynamicSelectQueryBuilder OrderBy(params DynamicColumn[] columns) + public virtual IDynamicSelectQueryBuilder OrderByColumn(params DynamicColumn[] columns) { foreach (var col in columns) OrderBy(x => col.ToSQLOrderByColumn(Database)); @@ -1068,9 +1094,9 @@ namespace DynamORM.Builders.Implementation /// Column format consist of Column Name and /// Alias in this order separated by ':'. /// Builder instance. - public virtual IDynamicSelectQueryBuilder OrderBy(params string[] columns) + public virtual IDynamicSelectQueryBuilder OrderByColumn(params string[] columns) { - return OrderBy(columns.Select(c => DynamicColumn.ParseOrderByColumn(c)).ToArray()); + return OrderByColumn(columns.Select(c => DynamicColumn.ParseOrderByColumn(c)).ToArray()); } #endregion OrderBy diff --git a/DynamORM/Builders/Implementation/DynamicUpdateQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicUpdateQueryBuilder.cs index 99f78ca..028ac02 100644 --- a/DynamORM/Builders/Implementation/DynamicUpdateQueryBuilder.cs +++ b/DynamORM/Builders/Implementation/DynamicUpdateQueryBuilder.cs @@ -152,7 +152,7 @@ namespace DynamORM.Builders.Implementation /// /// The specifications. /// This instance to permit chaining. - public virtual IDynamicUpdateQueryBuilder Values(params Func[] func) + public virtual IDynamicUpdateQueryBuilder Set(params Func[] func) { if (func == null) throw new ArgumentNullException("Array of specifications cannot be null."); diff --git a/DynamORM/DynamicDatabase.cs b/DynamORM/DynamicDatabase.cs index 4e15411..dbeafe5 100644 --- a/DynamORM/DynamicDatabase.cs +++ b/DynamORM/DynamicDatabase.cs @@ -94,9 +94,13 @@ namespace DynamORM /// Gets schema columns cache. internal Dictionary> Schema { get; private set; } +#if !DYNAMORM_OMMIT_OLDSYNTAX + /// Gets tables cache for this database instance. internal Dictionary TablesCache { get; private set; } +#endif + #endregion Internal fields and properties #region Properties and Constructors @@ -160,13 +164,17 @@ namespace DynamORM TransactionPool = new Dictionary>(); CommandsPool = new Dictionary>(); Schema = new Dictionary>(); +#if !DYNAMORM_OMMIT_OLDSYNTAX TablesCache = new Dictionary(); +#endif } #endregion Properties and Constructors #region Table +#if !DYNAMORM_OMMIT_OLDSYNTAX + /// Gets dynamic table which is a simple ORM using dynamic objects. /// The action with instance of as parameter. /// Table name. @@ -236,6 +244,8 @@ namespace DynamORM TablesCache.Remove(item.Key); } +#endif + #endregion Table #region From/Insert/Update/Delete @@ -248,11 +258,11 @@ namespace DynamORM /// - Generic expression: x => x( expression ).As( x.Alias ), where the alias part is mandatory. In this /// case the alias is not annotated. /// - /// The specification. + /// The specification. /// This instance to permit chaining. - public virtual IDynamicSelectQueryBuilder From(params Func[] func) + public virtual IDynamicSelectQueryBuilder From(Func fn) { - return new DynamicSelectQueryBuilder(this).From(func); + return new DynamicSelectQueryBuilder(this).From(fn); } /// Adds to the FROM clause using . @@ -263,6 +273,14 @@ namespace DynamORM return new DynamicSelectQueryBuilder(this).From(x => x(typeof(T))); } + /// Adds to the FROM clause using . + /// Type which can be represented in database. + /// This instance to permit chaining. + public virtual IDynamicSelectQueryBuilder From(Type t) + { + return new DynamicSelectQueryBuilder(this).From(x => x(t)); + } + /// /// Adds to the INSERT INTO clause the contents obtained by parsing the dynamic lambda expressions given. The supported /// formats are: @@ -287,6 +305,14 @@ namespace DynamORM return new DynamicInsertQueryBuilder(this).Table(typeof(T)); } + /// Adds to the INSERT INTO clause using . + /// Type which can be represented in database. + /// This instance to permit chaining. + public virtual IDynamicInsertQueryBuilder Insert(Type t) + { + return new DynamicInsertQueryBuilder(this).Table(t); + } + /// Bulk insert objects into database. /// Type of objects to insert. /// Enumerable containing instances of objects to insert. @@ -413,6 +439,14 @@ namespace DynamORM return new DynamicUpdateQueryBuilder(this).Table(typeof(T)); } + /// Adds to the UPDATE clause using . + /// Type which can be represented in database. + /// This instance to permit chaining. + public virtual IDynamicUpdateQueryBuilder Update(Type t) + { + return new DynamicUpdateQueryBuilder(this).Table(t); + } + /// Bulk update objects in database. /// Type of objects to update. /// Enumerable containing instances of objects to update. @@ -1231,10 +1265,12 @@ namespace DynamORM /// releasing, or resetting unmanaged resources. public void Dispose() { +#if !DYNAMORM_OMMIT_OLDSYNTAX var tables = TablesCache.Values.ToList(); TablesCache.Clear(); tables.ForEach(t => t.Dispose()); +#endif foreach (var con in TransactionPool) { diff --git a/DynamORM/DynamicExtensions.cs b/DynamORM/DynamicExtensions.cs index e092621..e6a6157 100644 --- a/DynamORM/DynamicExtensions.cs +++ b/DynamORM/DynamicExtensions.cs @@ -601,6 +601,8 @@ namespace DynamORM #region Generic Execution +#if !DYNAMORM_OMMIT_GENERICEXECUTION && !DYNAMORM_OMMIT_TRYPARSE + /// Execute scalar and return string if possible. /// Type to parse to. /// which will be executed. @@ -725,6 +727,8 @@ namespace DynamORM } } +#endif + #endregion Generic Execution /// Dump command into text writer. @@ -841,22 +845,49 @@ namespace DynamORM /// Creates sub query that can be used inside of from/join/expressions. /// Class implementing interface. /// The builder that will be parent of new sub query. + /// Instance of sub query. + public static IDynamicSelectQueryBuilder SubQuery(this T b) where T : IDynamicQueryBuilder + { + return new DynamicSelectQueryBuilder(b.Database, b as DynamicQueryBuilder); + } + + /// Creates sub query that can be used inside of from/join/expressions. + /// Class implementing interface. + /// The builder that will be parent of new sub query. + /// The specification for sub query. /// The specification for sub query. /// Instance of sub query. - public static IDynamicSelectQueryBuilder SubQuery(this T b, params Func[] func) where T : IDynamicQueryBuilder + public static IDynamicSelectQueryBuilder SubQuery(this T b, Func fn, params Func[] func) where T : IDynamicQueryBuilder { - return func == null || func.Length == 0 ? new DynamicSelectQueryBuilder(b.Database, b as DynamicQueryBuilder) : new DynamicSelectQueryBuilder(b.Database, b as DynamicQueryBuilder).From(func); + return new DynamicSelectQueryBuilder(b.Database, b as DynamicQueryBuilder).From(fn, func); } /// Creates sub query that can be used inside of from/join/expressions. /// Class implementing interface. /// The builder that will be parent of new sub query. /// First argument is parent query, second one is a sub query. + /// This instance to permit chaining. + public static T SubQuery(this T b, Action subquery) where T : IDynamicQueryBuilder + { + var sub = b.SubQuery(); + + subquery(b, sub); + + (b as DynamicQueryBuilder).ParseCommand(sub as DynamicQueryBuilder, b.Parameters); + + return b; + } + + /// Creates sub query that can be used inside of from/join/expressions. + /// Class implementing interface. + /// The builder that will be parent of new sub query. + /// First argument is parent query, second one is a sub query. + /// The specification for sub query. /// The specification for sub query. /// This instance to permit chaining. - public static T SubQuery(this T b, Action subquery, params Func[] func) where T : IDynamicQueryBuilder + public static T SubQuery(this T b, Action subquery, Func fn, params Func[] func) where T : IDynamicQueryBuilder { - var sub = b.SubQuery(func); + var sub = b.SubQuery(fn, func); subquery(b, sub); @@ -1244,6 +1275,8 @@ namespace DynamORM #region TryParse extensions +#if !DYNAMORM_OMMIT_TRYPARSE + /// Generic try parse. /// Type to parse to. /// Value to parse. @@ -1288,6 +1321,8 @@ namespace DynamORM /// Returns true if conversion was successful. public delegate bool TryParseHandler(string value, out T result); +#endif + #endregion TryParse extensions #region Coalesce - besicaly not an extensions diff --git a/DynamORM/DynamicTable.cs b/DynamORM/DynamicTable.cs index 9930fa9..e6f1839 100644 --- a/DynamORM/DynamicTable.cs +++ b/DynamORM/DynamicTable.cs @@ -41,6 +41,8 @@ using DynamORM.Mapper; namespace DynamORM { +#if !DYNAMORM_OMMIT_OLDSYNTAX + /// Dynamic table is a simple ORM using dynamic objects. /// /// Assume that we have a table representing Users class. @@ -836,25 +838,25 @@ namespace DynamORM { case "order": if (args[i] is string) - builder.OrderBy(((string)args[i]).Split(',')); + builder.OrderByColumn(((string)args[i]).Split(',')); else if (args[i] is string[]) - builder.OrderBy(args[i] as string); + builder.OrderByColumn(args[i] as string); else if (args[i] is DynamicColumn[]) - builder.OrderBy((DynamicColumn[])args[i]); + builder.OrderByColumn((DynamicColumn[])args[i]); else if (args[i] is DynamicColumn) - builder.OrderBy((DynamicColumn)args[i]); + builder.OrderByColumn((DynamicColumn)args[i]); else goto default; break; case "group": if (args[i] is string) - builder.GroupBy(((string)args[i]).Split(',')); + builder.GroupByColumn(((string)args[i]).Split(',')); else if (args[i] is string[]) - builder.GroupBy(args[i] as string); + builder.GroupByColumn(args[i] as string); else if (args[i] is DynamicColumn[]) - builder.GroupBy((DynamicColumn[])args[i]); + builder.GroupByColumn((DynamicColumn[])args[i]); else if (args[i] is DynamicColumn) - builder.GroupBy((DynamicColumn)args[i]); + builder.GroupByColumn((DynamicColumn)args[i]); else goto default; break; @@ -864,7 +866,7 @@ namespace DynamORM op.ToUpper() : null; if (args[i] is string || args[i] is string[]) - builder.Select((args[i] as String).NullOr(s => s.Split(','), args[i] as String[]) + builder.SelectColumn((args[i] as String).NullOr(s => s.Split(','), args[i] as String[]) .Select(c => { var col = DynamicColumn.ParseSelectColumn(c); @@ -874,7 +876,7 @@ namespace DynamORM return col; }).ToArray()); else if (args[i] is DynamicColumn || args[i] is DynamicColumn[]) - builder.Select((args[i] as DynamicColumn).NullOr(c => new DynamicColumn[] { c }, args[i] as DynamicColumn[]) + builder.SelectColumn((args[i] as DynamicColumn).NullOr(c => new DynamicColumn[] { c }, args[i] as DynamicColumn[]) .Select(c => { if (string.IsNullOrEmpty(c.Aggregate)) @@ -1042,4 +1044,6 @@ namespace DynamORM #endregion ICloneable Members } + +#endif } \ No newline at end of file diff --git a/DynamORM/Properties/AssemblyInfo.cs b/DynamORM/Properties/AssemblyInfo.cs index 9b55684..81c6c10 100644 --- a/DynamORM/Properties/AssemblyInfo.cs +++ b/DynamORM/Properties/AssemblyInfo.cs @@ -26,6 +26,11 @@ * THE POSSIBILITY OF SUCH DAMAGE. * * See: http://opensource.org/licenses/bsd-license.php + * + * Supported preprocessor flags: + * * DYNAMORM_OMMIT_OLDSYNTAX - Remove dynamic table functionality + * * DYNAMORM_OMMIT_GENERICEXECUTION - Remove generic execution functionality + * * DYNAMORM_OMMIT_TRYPARSE - Remove TryParse helpers (also applies DYNAMORM_OMMIT_GENERICEXECUTION) */ using System.Reflection;