//******************************************************************************************************
// TableOperations.cs - Gbtc
//
// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
// file except in compliance with the License. You may obtain a copy of the License at:
//
// http://opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 02/01/2016 - J. Ritchie Carroll
// Generated original version of source code.
//
//******************************************************************************************************
using ExpressionEvaluator;
using GSF.Collections;
using GSF.ComponentModel;
using GSF.Reflection;
using GSF.Security.Cryptography;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Data.Common;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
// ReSharper disable UnusedMember.Global
// ReSharper disable StaticMemberInGenericType
// ReSharper disable UnusedMember.Local
// ReSharper disable AssignNullToNotNullAttribute
// ReSharper disable NotAccessedField.Local
namespace GSF.Data.Model
{
///
/// Defines database operations for a modeled table.
///
/// Modeled table.
[SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")]
public class TableOperations : ITableOperations where T : class, new()
{
#region [ Members ]
// Nested Types
private class CurrentScope : ValueExpressionScopeBase
{
// Define instance variables exposed to ValueExpressionAttributeBase expressions
#pragma warning disable 169, 414, 649
public TableOperations TableOperations;
public AdoDataConnection Connection;
#pragma warning restore 169, 414, 649
}
private class NullConnection : IDbConnection
{
public string ConnectionString { get; set; }
public int ConnectionTimeout { get; } = 0;
public string Database { get; } = null;
public ConnectionState State { get; } = ConnectionState.Open;
public void Open() { }
public void Close() { }
public void Dispose() { }
public void ChangeDatabase(string databaseName) { }
public IDbCommand CreateCommand() => null;
public IDbTransaction BeginTransaction() => null;
public IDbTransaction BeginTransaction(IsolationLevel il) => null;
}
private class IntermediateParameter : IDbDataParameter
{
public DbType DbType { get; set; }
public ParameterDirection Direction { get; set; }
public bool IsNullable { get; } = false;
public string ParameterName { get; set; }
public string SourceColumn { get; set; }
public DataRowVersion SourceVersion { get; set; }
public object Value { get; set; }
public byte Precision { get; set; }
public byte Scale { get; set; }
public int Size { get; set; }
}
// Constants
private const string SelectCountSqlFormat = "SELECT COUNT(*) FROM {0}";
private const string SelectSetSqlFormat = "SELECT {0} FROM {1} ORDER BY {{0}}";
private const string SelectSetWhereSqlFormat = "SELECT {0} FROM {1} WHERE {{0}} ORDER BY {{1}}";
private const string SelectRowSqlFormat = "SELECT * FROM {0} WHERE {1}";
private const string AddNewSqlFormat = "INSERT INTO {0}({1}) VALUES ({2})";
private const string UpdateSqlFormat = "UPDATE {0} SET {1} WHERE {2}";
private const string DeleteSqlFormat = "DELETE FROM {0} WHERE {1}";
private const string TableNamePrefixToken = "";
private const string TableNameSuffixToken = "";
private const string FieldListPrefixToken = "";
private const string FieldListSuffixToken = "";
// Fields
private readonly string m_selectCountSql;
private readonly string m_selectSetSql;
private readonly string m_selectSetWhereSql;
private readonly string m_selectKeysSql;
private readonly string m_selectKeysWhereSql;
private readonly string m_selectRowSql;
private readonly string m_addNewSql;
private readonly string m_updateSql;
private readonly string m_updateWhereSql;
private readonly string m_deleteSql;
private readonly string m_deleteWhereSql;
private readonly string m_searchFilterSql;
private string m_lastSortField;
private RecordRestriction m_lastRestriction;
#endregion
#region [ Constructors ]
///
/// Creates a new .
///
/// instance to use for database operations.
/// Custom run-time tokens to apply to any modeled values.
/// cannot be null.
///
/// The can be used to apply run-time tokens to any defined values,
/// for example, given the following amendment expression applied to a modeled class:
///
/// [AmendExpression("TOP {count}",
/// TargetExpression = TargetExpression.FieldList,
/// AffixPosition = AffixPosition.Prefix,
/// StatementTypes = StatementTypes.SelectSet)]]
///
/// The key/value pairs could be set as follows at run-time:
///
/// int count = 200;
/// customTokens = new[] { new KeyValuePair<string, string>("{count}", $"{count}") };
///
///
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
public TableOperations(AdoDataConnection connection, IEnumerable> customTokens = null)
{
if ((object)connection == null)
throw new ArgumentNullException(nameof(connection));
Connection = connection;
m_selectCountSql = s_selectCountSql;
m_selectSetSql = s_selectSetSql;
m_selectSetWhereSql = s_selectSetWhereSql;
m_selectKeysSql = s_selectKeysSql;
m_selectKeysWhereSql = s_selectKeysWhereSql;
m_selectRowSql = s_selectRowSql;
m_addNewSql = s_addNewSql;
m_updateSql = s_updateSql;
m_updateWhereSql = s_updateWhereSql;
m_deleteSql = s_deleteSql;
m_deleteWhereSql = s_deleteWhereSql;
m_searchFilterSql = s_searchFilterSql;
// Establish any modeled root query restriction parameters
if ((object)s_rootQueryRestrictionAttribute != null)
{
RootQueryRestriction = new RecordRestriction(s_rootQueryRestrictionAttribute.FilterExpression, s_rootQueryRestrictionAttribute.Parameters);
ApplyRootQueryRestrictionToUpdates = s_rootQueryRestrictionAttribute.ApplyToUpdates;
ApplyRootQueryRestrictionToDeletes = s_rootQueryRestrictionAttribute.ApplyToDeletes;
}
// When any escape targets are defined for the modeled identifiers, i.e., table or field names,
// the static SQL statements are defined with ANSI standard escape delimiters. We check if the
// user model has opted to instead use common database escape delimiters, or no delimiters, that
// will apply to the active database type and make any needed adjustments. As a result, it will
// be slightly faster to construct this class when ANSI standard escape delimiters are used.
if ((object)s_escapedTableNameTargets != null)
{
string derivedTableName = GetEscapedTableName();
string ansiEscapedTableName = $"\"{s_tableName}\"";
if (!derivedTableName.Equals(ansiEscapedTableName))
{
m_selectCountSql = m_selectCountSql.Replace(ansiEscapedTableName, derivedTableName);
m_selectSetSql = m_selectSetSql.Replace(ansiEscapedTableName, derivedTableName);
m_selectSetWhereSql = m_selectSetWhereSql.Replace(ansiEscapedTableName, derivedTableName);
m_selectKeysSql = m_selectKeysSql.Replace(ansiEscapedTableName, derivedTableName);
m_selectKeysWhereSql = m_selectKeysWhereSql.Replace(ansiEscapedTableName, derivedTableName);
m_selectRowSql = m_selectRowSql.Replace(ansiEscapedTableName, derivedTableName);
m_addNewSql = m_addNewSql.Replace(ansiEscapedTableName, derivedTableName);
m_updateSql = m_updateSql.Replace(ansiEscapedTableName, derivedTableName);
m_updateWhereSql = m_updateWhereSql.Replace(ansiEscapedTableName, derivedTableName);
m_deleteSql = m_deleteSql.Replace(ansiEscapedTableName, derivedTableName);
m_deleteWhereSql = m_deleteWhereSql.Replace(ansiEscapedTableName, derivedTableName);
}
}
if ((object)s_escapedFieldNameTargets != null)
{
foreach (KeyValuePair> escapedFieldNameTarget in s_escapedFieldNameTargets)
{
string fieldName = escapedFieldNameTarget.Key;
string derivedFieldName = GetEscapedFieldName(fieldName, escapedFieldNameTarget.Value);
string ansiEscapedFieldName = $"\"{fieldName}\"";
if (!derivedFieldName.Equals(ansiEscapedFieldName))
{
m_selectKeysSql = m_selectKeysSql.Replace(ansiEscapedFieldName, derivedFieldName);
m_selectKeysWhereSql = m_selectKeysWhereSql.Replace(ansiEscapedFieldName, derivedFieldName);
m_selectRowSql = m_selectRowSql.Replace(ansiEscapedFieldName, derivedFieldName);
m_addNewSql = m_addNewSql.Replace(ansiEscapedFieldName, derivedFieldName);
m_updateSql = m_updateSql.Replace(ansiEscapedFieldName, derivedFieldName);
m_updateWhereSql = m_updateWhereSql.Replace(ansiEscapedFieldName, derivedFieldName);
m_deleteSql = m_deleteSql.Replace(ansiEscapedFieldName, derivedFieldName);
m_deleteWhereSql = m_deleteWhereSql.Replace(ansiEscapedFieldName, derivedFieldName);
m_searchFilterSql = m_searchFilterSql.Replace(ansiEscapedFieldName, derivedFieldName);
}
}
}
// Handle any modeled expression amendments
if ((object)s_expressionAmendments != null)
{
foreach (Tuple expressionAmendment in s_expressionAmendments)
{
// See if expression amendment applies to current database type
if (expressionAmendment.Item1 != Connection.DatabaseType)
continue;
// Get expression amendment properties
TargetExpression targetExpression = expressionAmendment.Item2;
StatementTypes statementTypes = expressionAmendment.Item3;
AffixPosition affixPosition = expressionAmendment.Item4;
string amendmentText = expressionAmendment.Item5;
string tableNameToken = affixPosition == AffixPosition.Prefix ? TableNamePrefixToken : TableNameSuffixToken;
string fieldListToken = affixPosition == AffixPosition.Prefix ? FieldListPrefixToken : FieldListSuffixToken;
string targetToken = targetExpression == TargetExpression.TableName ? tableNameToken : fieldListToken;
// Apply amendment to target statement types
if (statementTypes.HasFlag(StatementTypes.SelectCount) && targetExpression == TargetExpression.TableName)
m_selectCountSql = m_selectCountSql.Replace(targetToken, amendmentText);
if (statementTypes.HasFlag(StatementTypes.SelectSet))
{
m_selectSetSql = m_selectSetSql.Replace(targetToken, amendmentText);
m_selectSetWhereSql = m_selectSetWhereSql.Replace(targetToken, amendmentText);
m_selectKeysSql = m_selectKeysSql.Replace(targetToken, amendmentText);
m_selectKeysWhereSql = m_selectKeysWhereSql.Replace(targetToken, amendmentText);
}
if (statementTypes.HasFlag(StatementTypes.SelectRow))
m_selectRowSql = m_selectRowSql.Replace(targetToken, amendmentText);
if (statementTypes.HasFlag(StatementTypes.Insert))
m_addNewSql = m_addNewSql.Replace(targetToken, amendmentText);
if (statementTypes.HasFlag(StatementTypes.Update))
{
m_updateSql = m_updateSql.Replace(targetToken, amendmentText);
m_updateWhereSql = m_updateWhereSql.Replace(targetToken, amendmentText);
}
if (statementTypes.HasFlag(StatementTypes.Delete))
{
m_deleteSql = m_deleteSql.Replace(targetToken, amendmentText);
m_deleteWhereSql = m_deleteWhereSql.Replace(targetToken, amendmentText);
}
}
// Remove any remaining tokens from instance expressions
string RemoveRemainingTokens(string sql) => sql
.Replace(TableNamePrefixToken, "")
.Replace(TableNameSuffixToken, "")
.Replace(FieldListPrefixToken, "")
.Replace(FieldListSuffixToken, "");
m_selectCountSql = RemoveRemainingTokens(m_selectCountSql);
m_selectSetSql = RemoveRemainingTokens(m_selectSetSql);
m_selectSetWhereSql = RemoveRemainingTokens(m_selectSetWhereSql);
m_selectKeysSql = RemoveRemainingTokens(m_selectKeysSql);
m_selectKeysWhereSql = RemoveRemainingTokens(m_selectKeysWhereSql);
m_selectRowSql = RemoveRemainingTokens(m_selectRowSql);
m_addNewSql = RemoveRemainingTokens(m_addNewSql);
m_updateSql = RemoveRemainingTokens(m_updateSql);
m_updateWhereSql = RemoveRemainingTokens(m_updateWhereSql);
m_deleteSql = RemoveRemainingTokens(m_deleteSql);
m_deleteWhereSql = RemoveRemainingTokens(m_deleteWhereSql);
// Execute replacements on any provided custom run-time tokens
if ((object)customTokens != null)
{
foreach (KeyValuePair customToken in customTokens)
{
m_selectCountSql = m_selectCountSql.Replace(customToken.Key, customToken.Value);
m_selectSetSql = m_selectSetSql.Replace(customToken.Key, customToken.Value);
m_selectSetWhereSql = m_selectSetWhereSql.Replace(customToken.Key, customToken.Value);
m_selectKeysSql = m_selectKeysSql.Replace(customToken.Key, customToken.Value);
m_selectKeysWhereSql = m_selectKeysWhereSql.Replace(customToken.Key, customToken.Value);
m_selectRowSql = m_selectRowSql.Replace(customToken.Key, customToken.Value);
m_addNewSql = m_addNewSql.Replace(customToken.Key, customToken.Value);
m_updateSql = m_updateSql.Replace(customToken.Key, customToken.Value);
m_updateWhereSql = m_updateWhereSql.Replace(customToken.Key, customToken.Value);
m_deleteSql = m_deleteSql.Replace(customToken.Key, customToken.Value);
m_deleteWhereSql = m_deleteWhereSql.Replace(customToken.Key, customToken.Value);
}
}
}
}
///
/// Creates a new using provided .
///
/// instance to use for database operations.
/// Delegate to handle table operation exceptions.
/// Custom run-time tokens to apply to any modeled values.
///
///
/// When exception handler is provided, table operations will not throw exceptions for database calls, any
/// encountered exceptions will be passed to handler for processing.
///
///
/// The can be used to apply run-time tokens to any defined values,
/// for example, given the following amendment expression applied to a modeled class:
///
/// [AmendExpression("TOP {count}",
/// TargetExpression = TargetExpression.FieldList,
/// AffixPosition = AffixPosition.Prefix,
/// StatementTypes = StatementTypes.SelectSet)]]
///
/// The key/value pairs could be set as follows at run-time:
///
/// int count = 200;
/// customTokens = new[] { new KeyValuePair<string, string>("{count}", $"{count}") };
///
///
///
/// cannot be null.
public TableOperations(AdoDataConnection connection, Action exceptionHandler, IEnumerable> customTokens = null)
: this(connection, customTokens) => ExceptionHandler = exceptionHandler;
#endregion
#region [ Properties ]
///
/// Gets instance associated with this used for database operations.
///
public AdoDataConnection Connection { get; }
///
/// Gets the table name defined for the modeled table, includes any escaping as defined in model.
///
public string TableName => GetEscapedTableName();
///
/// Gets the table name defined for the modeled table without any escape characters.
///
///
/// A table name will only be escaped if the model requested escaping with the .
///
public string UnescapedTableName => s_tableName;
///
/// Gets flag that determines if modeled table has a primary key that is an identity field.
///
public bool HasPrimaryKeyIdentityField => s_hasPrimaryKeyIdentityField;
///
/// Gets or sets delegate used to handle table operation exceptions.
///
///
/// When exception handler is provided, table operations will not throw exceptions for database calls, any
/// encountered exceptions will be passed to handler for processing. Otherwise, exceptions will be thrown
/// on the call stack.
///
public Action ExceptionHandler { get; set; }
///
/// Gets or sets flag that determines if field names should be treated as case sensitive. Defaults to false.
///
///
/// In cases where modeled table fields have applied , this flag will be used
/// to properly update escaped field names that may be case sensitive. For example, escaped field names in Oracle
/// are case sensitive. This value is typically false.
///
public bool UseCaseSensitiveFieldNames { get; set; }
///
/// Gets or sets primary key cache.
///
///
///
/// The overloads that include paging parameters
/// cache the sorted and filtered primary keys of queried records between calls so that paging is fast and
/// efficient. Since the primary keys are cached, an instance of the should
/// exist per user session when using query functions that support pagination. In web based implementations,
/// the primary cache should be stored with user session state data and then restored between instances of
/// the that are created along with a connection that is opened per page.
///
///
/// The function should be called to manually clear cache when table
/// contents are known to have changed. Note that calls to any overloads will
/// automatically clear any existing primary key cache.
///
///
/// Primary keys values are stored in data table without interpretation, i.e., in their raw form as queried
/// from the database. Primary key data in cache will be encrypted for models with primary key fields that
/// are marked with the
///
///
public DataTable PrimaryKeyCache { get; set; }
///
/// Gets or sets root record restriction that applies to query table operations.
///
///
///
/// Defining a root query restriction creates a base query filter that gets applied to all query operations,
/// even when another restriction is applied - in this case the root restriction will be pre-pended to the
/// specified query, e.g.:
///
/// restriction = RootQueryRestriction + restriction;
///
/// A root query restriction is useful to apply a common state to the query operations, e.g., always
/// filtering records for a specific user or context.
///
///
/// A root query restriction can be manually assigned to a instance or
/// automatically assigned by marking a model with the .
///
///
/// If any of the reference a table field that is modeled with
/// either an or , then the function
/// will need to be called, replacing the target parameter with the
/// returned value so that the field value will be properly set prior to executing the database function.
///
///
public RecordRestriction RootQueryRestriction { get; set; }
///
/// Gets or sets flag that determines if should be applied to update operations.
///
///
///
/// If only references primary key fields, then this property value should be set
/// to false since default update operations for a modeled record already work against primary key fields.
///
///
/// This flag can be manually set per instance or handled automatically by marking
/// a model with the and assigning a value to the attribute property
/// .
///
///
public bool ApplyRootQueryRestrictionToUpdates { get; set; }
///
/// Gets or sets flag that determines if should be applied to delete operations.
///
///
///
/// If only references primary key fields, then this property value should be set
/// to false since default delete operations for a modeled record already work against primary key fields.
///
///
/// This flag can be manually set per instance or handled automatically by marking
/// a model with the and assigning a value to the attribute property
/// .
///
///
public bool ApplyRootQueryRestrictionToDeletes { get; set; }
#endregion
#region [ Methods ]
///
/// Creates a new modeled record instance, applying any modeled default values as specified by a
/// or on the
/// model properties.
///
/// New modeled record instance with any defined default values applied.
public T NewRecord()
{
try
{
return s_createRecordInstance(new CurrentScope
{
TableOperations = this,
Connection = Connection
});
}
catch (Exception ex)
{
if ((object)ExceptionHandler == null)
throw;
ExceptionHandler(ex);
return null;
}
}
object ITableOperations.NewRecord() => NewRecord();
///
/// Applies the default values on the specified modeled table
/// where any of the properties are marked with either
/// or .
///
/// Record to update.
public void ApplyRecordDefaults(T record)
{
try
{
s_applyRecordDefaults(new CurrentScope
{
Instance = record,
TableOperations = this,
Connection = Connection
});
}
catch (Exception ex)
{
if ((object)ExceptionHandler == null)
throw;
ExceptionHandler(ex);
}
}
void ITableOperations.ApplyRecordDefaults(object value)
{
if (!(value is T record))
throw new ArgumentException($"Cannot apply defaults for record of type \"{value?.GetType().Name ?? "null"}\", expected \"{typeof(T).Name}\"", nameof(value));
ApplyRecordDefaults(record);
}
///
/// Applies the update values on the specified modeled table where
/// any of the properties are marked with .
///
/// Record to update.
public void ApplyRecordUpdates(T record)
{
try
{
s_updateRecordInstance(new CurrentScope
{
Instance = record,
TableOperations = this,
Connection = Connection
});
}
catch (Exception ex)
{
if ((object)ExceptionHandler == null)
throw;
ExceptionHandler(ex);
}
}
void ITableOperations.ApplyRecordUpdates(object value)
{
if (!(value is T record))
throw new ArgumentException($"Cannot apply updates for record of type \"{value?.GetType().Name ?? "null"}\", expected \"{typeof(T).Name}\"", nameof(value));
ApplyRecordUpdates(record);
}
///
/// Queries database and returns a single modeled table record for the specified .
///
/// Record restriction to apply.
/// A single modeled table record for the queried record.
///
///
/// If no record is found for specified , null will be returned.
///
///
/// This is a convenience call to
/// specifying the parameter with a limit of 1 record.
///
///
/// If any of the parameters reference a table field that is modeled with
/// either an or , then the function
/// will need to be called, replacing the target parameter with the
/// returned value so that the field value will be properly set prior to executing the database function.
///
///
public T QueryRecord(RecordRestriction restriction) => QueryRecord(null, restriction);
object ITableOperations.QueryRecord(RecordRestriction restriction) => QueryRecord(restriction);
///
/// Queries database and returns a single modeled table record for the specified ,
/// execution of query will apply .
///
/// Field name expression used for sort order, include ASC or DESC as needed - does not include ORDER BY; defaults to primary keys.
/// Record restriction to apply.
/// A single modeled table record for the queried record.
///
///
/// If no record is found for specified , null will be returned.
///
///
/// This is a convenience call to
/// specifying the parameter with a limit of 1 record.
///
///
/// If any of the parameters reference a table field that is modeled with
/// either an or , then the function
/// will need to be called, replacing the target parameter with the
/// returned value so that the field value will be properly set prior to executing the database function.
///
///
public T QueryRecord(string orderByExpression, RecordRestriction restriction) => QueryRecords(orderByExpression, restriction, 1).FirstOrDefault();
object ITableOperations.QueryRecord(string orderByExpression, RecordRestriction restriction) => QueryRecord(orderByExpression, restriction);
///
/// Queries database and returns a single modeled table record for the specified SQL filter
/// expression and parameters.
///
///
/// Filter SQL expression for restriction as a composite format string - does not include WHERE.
/// When escaping is needed for field names, use standard ANSI quotes.
///
/// Restriction parameter values.
/// A single modeled table record for the queried record.
///
///
/// If no record is found for specified filter expression and parameters, null will be returned.
///
///
/// Each indexed parameter, e.g., "{0}", in the composite format
/// will be converted into query parameters where each of the corresponding values in the
/// collection will be applied as
/// values to an executed query.
///
///
/// If any of the specified reference a table field that is modeled with
/// either an or , then the function
/// will need to be called, replacing the target parameter with the
/// returned value so that the field value will be properly set prior to executing the database function.
///
///
/// If needed, field names that are escaped with standard ANSI quotes in the filter expression
/// will be updated to reflect what is defined in the user model.
///
///
/// This is a convenience call to
/// specifying the parameter with a limit of 1 record.
///
///
public T QueryRecordWhere(string filterExpression, params object[] parameters) => QueryRecord(new RecordRestriction(filterExpression, parameters));
object ITableOperations.QueryRecordWhere(string filterExpression, params object[] parameters) => QueryRecordWhere(filterExpression, parameters);
///
/// Queries database and returns modeled table records for the specified parameters.
///
/// Field name expression used for sort order, include ASC or DESC as needed - does not include ORDER BY; defaults to primary keys.
/// Record restriction to apply, if any.
/// Limit of number of record to return.
/// An enumerable of modeled table row instances for queried records.
///
///
/// If no record or is provided, all rows will be returned.
///
///
/// If any of the parameters reference a table field that is modeled with
/// either an or , then the function
/// will need to be called, replacing the target parameter with the
/// returned value so that the field value will be properly set prior to executing the database function.
///
///
public IEnumerable QueryRecords(string orderByExpression = null, RecordRestriction restriction = null, int limit = -1)
{
if (string.IsNullOrWhiteSpace(orderByExpression))
orderByExpression = UpdateFieldNames(s_primaryKeyFields);
string sqlExpression = null;
try
{
if ((object)RootQueryRestriction != null)
restriction = RootQueryRestriction + restriction;
if (limit < 1)
{
// No record limit specified
if ((object)restriction == null)
{
sqlExpression = string.Format(m_selectSetSql, orderByExpression);
return Connection.RetrieveData(sqlExpression).AsEnumerable().Select(LoadRecord);
}
sqlExpression = string.Format(m_selectSetWhereSql, UpdateFieldNames(restriction.FilterExpression), orderByExpression);
return Connection.RetrieveData(sqlExpression, restriction.Parameters).AsEnumerable().Select(LoadRecord);
}
if ((object)restriction == null)
{
sqlExpression = string.Format(m_selectSetSql, orderByExpression);
return Connection.RetrieveData(sqlExpression).AsEnumerable().Take(limit).Select(LoadRecord);
}
sqlExpression = string.Format(m_selectSetWhereSql, UpdateFieldNames(restriction.FilterExpression), orderByExpression);
return Connection.RetrieveData(sqlExpression, restriction.Parameters).AsEnumerable().Take(limit).Select(LoadRecord);
}
catch (Exception ex)
{
InvalidOperationException opex = new InvalidOperationException($"Exception during record query for {typeof(T).Name} \"{sqlExpression ?? "undefined"}, {ValueList(restriction?.Parameters)}\": {ex.Message}", ex);
if ((object)ExceptionHandler == null)
throw opex;
ExceptionHandler(opex);
return Enumerable.Empty();
}
}
IEnumerable ITableOperations.QueryRecords(string orderByExpression, RecordRestriction restriction, int limit) => QueryRecords(orderByExpression, restriction, limit);
///
/// Queries database and returns modeled table records for the specified .
///
/// Record restriction to apply.
/// An enumerable of modeled table row instances for queried records.
///
///
/// This is a convenience call to only
/// specifying the parameter.
///
///
/// If any of the parameters reference a table field that is modeled with
/// either an or , then the function
/// will need to be called, replacing the target parameter with the
/// returned value so that the field value will be properly set prior to executing the database function.
///
///
public IEnumerable QueryRecords(RecordRestriction restriction) => QueryRecords(null, restriction);
IEnumerable ITableOperations.QueryRecords(RecordRestriction restriction) => QueryRecords(restriction);
///
/// Queries database and returns modeled table records for the specified SQL filter expression
/// and parameters.
///
///
/// Filter SQL expression for restriction as a composite format string - does not include WHERE.
/// When escaping is needed for field names, use standard ANSI quotes.
///
/// Restriction parameter values.
/// An enumerable of modeled table row instances for queried records.
///
///
/// Each indexed parameter, e.g., "{0}", in the composite format
/// will be converted into query parameters where each of the corresponding values in the
/// collection will be applied as
/// values to an executed query.
///
///
/// If any of the specified reference a table field that is modeled with
/// either an or , then the function
/// will need to be called, replacing the target parameter with the
/// returned value so that the field value will be properly set prior to executing the database function.
///
///
/// If needed, field names that are escaped with standard ANSI quotes in the filter expression
/// will be updated to reflect what is defined in the user model.
///
///
/// This is a convenience call to only
/// specifying the parameter.
///
///
public IEnumerable QueryRecordsWhere(string filterExpression, params object[] parameters) => QueryRecords(new RecordRestriction(filterExpression, parameters));
IEnumerable ITableOperations.QueryRecordsWhere(string filterExpression, params object[] parameters) => QueryRecordsWhere(filterExpression, parameters);
///
/// Queries database and returns modeled table records for the specified sorting, paging and search parameters.
/// Search executed against fields modeled with .
///
/// Field name to order-by.
/// Sort ascending flag; set to false for descending.
/// Page number of records to return (1-based).
/// Current page size.
/// Text to search.
/// An enumerable of modeled table row instances for queried records.
///
///
/// This function is used for record paging. Primary keys are cached server-side, typically per user session,
/// to maintain desired per-page sort order. Call to manually clear cache
/// when table contents are known to have changed.
///
///
/// If the specified has been marked with ,
/// establishing the primary key cache operation will take longer to execute since query data will need to
/// be downloaded locally and decrypted so the proper sort order can be determined.
///
///
/// This is a convenience call to where restriction
/// is generated by using .
///
///
public IEnumerable QueryRecords(string sortField, bool ascending, int page, int pageSize, string searchText) => QueryRecords(sortField, ascending, page, pageSize, GetSearchRestriction(searchText));
IEnumerable ITableOperations.QueryRecords(string sortField, bool ascending, int page, int pageSize, string searchText) => QueryRecords(sortField, ascending, page, pageSize, searchText);
///
/// Queries database and returns modeled table records for the specified sorting and paging parameters.
///
/// Field name to order-by.
/// Sort ascending flag; set to false for descending.
/// Page number of records to return (1-based).
/// Current page size.
/// Record restriction to apply, if any.
/// An enumerable of modeled table row instances for queried records.
///
///
/// This function is used for record paging. Primary keys are cached server-side, typically per user session,
/// to maintain desired per-page sort order. Call to manually clear cache
/// when table contents are known to have changed.
///
///
/// If any of the parameters reference a table field that is modeled with
/// either an or , then the function
/// will need to be called, replacing the target parameter with the
/// returned value so that the field value will be properly set prior to executing the database function.
///
///
/// If the specified has been marked with ,
/// establishing the primary key cache operation will take longer to execute since query data will need to
/// be downloaded locally and decrypted so the proper sort order can be determined.
///
///
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
public IEnumerable QueryRecords(string sortField, bool ascending, int page, int pageSize, RecordRestriction restriction = null)
{
if (string.IsNullOrWhiteSpace(sortField))
sortField = s_fieldNames[s_primaryKeyProperties[0].Name];
bool sortFieldIsEncrypted = FieldIsEncrypted(sortField);
// Records that have been deleted since primary key cache was established will return null and be filtered out which will throw
// off the record count. Local delete operations automatically clear the primary key cache, however, if record set is known to
// have changed outside purview of this class, the "ClearPrimaryKeyCache()" method should be manually called so that primary key
// cache can be reestablished.
if ((object)PrimaryKeyCache == null || !sortField.Equals(m_lastSortField, StringComparison.OrdinalIgnoreCase) || restriction != m_lastRestriction)
{
string orderByExpression = sortFieldIsEncrypted ? s_fieldNames[s_primaryKeyProperties[0].Name] : $"{sortField}{(ascending ? "" : " DESC")}";
string sqlExpression = null;
try
{
if ((object)RootQueryRestriction != null)
restriction = RootQueryRestriction + restriction;
if ((object)restriction == null)
{
sqlExpression = string.Format(m_selectKeysSql, orderByExpression);
PrimaryKeyCache = Connection.RetrieveData(sqlExpression);
}
else
{
sqlExpression = string.Format(m_selectKeysWhereSql, UpdateFieldNames(restriction.FilterExpression), orderByExpression);
PrimaryKeyCache = Connection.RetrieveData(sqlExpression, restriction.Parameters);
}
// If sort field is encrypted, execute a local sort and update primary key cache
if (sortFieldIsEncrypted && s_propertyNames.TryGetValue(sortField, out string propertyName) && s_properties.TryGetValue(propertyName, out PropertyInfo sortFieldProperty))
{
// Reduce properties to load only primary key fields and sort field
HashSet properties = new HashSet(s_primaryKeyProperties) { sortFieldProperty };
IEnumerable sortResult = LocalOrderBy(PrimaryKeyCache.AsEnumerable().Select(row => LoadRecordFromCachedKeys(row.ItemArray, properties)).Where(record => record != null), sortField, ascending);
DataTable sortedKeyCache = new DataTable(s_tableName);
foreach (DataColumn column in PrimaryKeyCache.Columns)
sortedKeyCache.Columns.Add(column.ColumnName, column.DataType);
foreach (T record in sortResult)
sortedKeyCache.Rows.Add(GetPrimaryKeys(record));
PrimaryKeyCache = sortedKeyCache;
}
}
catch (Exception ex)
{
InvalidOperationException opex = new InvalidOperationException($"Exception during record query for {typeof(T).Name} \"{sqlExpression ?? "undefined"}, {ValueList(restriction?.Parameters)}\": {ex.Message}", ex);
if ((object)ExceptionHandler == null)
throw opex;
ExceptionHandler(opex);
return Enumerable.Empty();
}
m_lastSortField = sortField;
m_lastRestriction = restriction;
}
// Paginate on cached data rows so paging does no work except to skip through records, then only load records for a given page of data
return PrimaryKeyCache.AsEnumerable().ToPagedList(page, pageSize, PrimaryKeyCache.Rows.Count).Select(row => LoadRecordFromCachedKeys(row.ItemArray)).Where(record => record != null);
}
IEnumerable ITableOperations.QueryRecords(string sortField, bool ascending, int page, int pageSize, RecordRestriction restriction) => QueryRecords(sortField, ascending, page, pageSize, restriction);
///
/// Gets the record count for the modeled table based on search parameter.
/// Search executed against fields modeled with .
///
/// Text to search.
/// Record count for the modeled table based on search parameter.
///
/// This is a convenience call to where restriction
/// is generated by
///
public int QueryRecordCount(string searchText) => QueryRecordCount(GetSearchRestriction(searchText));
///
/// Gets the record count for the specified - or - total record
/// count for the modeled table if is null.
///
/// Record restriction to apply, if any.
///
/// Record count for the specified - or - total record count
/// for the modeled table if is null.
///
///
/// If any of the parameters reference a table field that is modeled with
/// either an or , then the function
/// will need to be called, replacing the target parameter with the
/// returned value so that the field value will be properly set prior to executing the database function.
///
public int QueryRecordCount(RecordRestriction restriction = null)
{
string sqlExpression = null;
try
{
if ((object)RootQueryRestriction != null)
restriction = RootQueryRestriction + restriction;
if ((object)restriction == null)
{
sqlExpression = m_selectCountSql;
return Connection.ExecuteScalar(sqlExpression);
}
sqlExpression = $"{m_selectCountSql} WHERE {UpdateFieldNames(restriction.FilterExpression)}";
return Connection.ExecuteScalar(sqlExpression, restriction.Parameters);
}
catch (Exception ex)
{
InvalidOperationException opex = new InvalidOperationException($"Exception during record count query for {typeof(T).Name} \"{sqlExpression ?? "undefined"}, {ValueList(restriction?.Parameters)}\": {ex.Message}", ex);
if ((object)ExceptionHandler == null)
throw opex;
ExceptionHandler(opex);
return -1;
}
}
///
/// Gets the record count for the modeled table for the specified SQL filter expression and parameters.
///
///
/// Filter SQL expression for restriction as a composite format string - does not include WHERE.
/// When escaping is needed for field names, use standard ANSI quotes.
///
/// Restriction parameter values.
/// Record count for the modeled table for the specified parameters.
///
///
/// Each indexed parameter, e.g., "{0}", in the composite format
/// will be converted into query parameters where each of the corresponding values in the
/// collection will be applied as
/// values to an executed query.
///
///
/// If any of the specified reference a table field that is modeled with
/// either an or , then the function
/// will need to be called, replacing the target parameter with the
/// returned value so that the field value will be properly set prior to executing the database function.
///
///
/// If needed, field names that are escaped with standard ANSI quotes in the filter expression
/// will be updated to reflect what is defined in the user model.
///
///
/// This is a convenience call to .
///
///
public int QueryRecordCountWhere(string filterExpression, params object[] parameters) => QueryRecordCount(new RecordRestriction(filterExpression, parameters));
///
/// Locally searches retrieved table records after queried from database for the specified sorting and search parameters.
/// Search executed against fields modeled with .
/// Function only typically used for record models that apply the .
///
/// Field name to order-by.
/// Sort ascending flag; set to false for descending.
/// Text to search.
/// to use when searching string fields; defaults to ordinal ignore case.
/// An array of modeled table row instances for the queried records that match the search.
///
///
/// This function searches records locally after query from database, this way functionality will work
/// even with fields that are modeled with the and use the .
/// Primary keys for this function will not be cached server-side and this function will be slower and more expensive than similar calls
/// to . Usage should be restricted to cases searching for field data that has
/// been modeled with the .
///
///
/// This function does not paginate records, instead a full list of search records is returned. User can cache returned records and page
/// through them using the function. As a result, usage should be restricted to smaller data sets.
///
///
public T[] SearchRecords(string sortField, bool ascending, string searchText, StringComparison comparison = StringComparison.OrdinalIgnoreCase)
{
if (string.IsNullOrWhiteSpace(m_searchFilterSql) || string.IsNullOrWhiteSpace(searchText))
return null;
if (string.IsNullOrWhiteSpace(sortField))
sortField = s_fieldNames[s_primaryKeyProperties[0].Name];
searchText = searchText.Trim();
string[] searchValues = searchText.RemoveDuplicateWhiteSpace().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
bool sortFieldIsEncrypted = FieldIsEncrypted(sortField);
string orderByExpression = sortFieldIsEncrypted ? null : $"{sortField}{(ascending ? "" : " DESC")}";
IEnumerable queryResult = QueryRecords(orderByExpression).Where(record => IsSearchMatch(record, comparison, searchValues));
if (sortFieldIsEncrypted)
queryResult = LocalOrderBy(queryResult, sortField, ascending, comparison.GetComparer());
return queryResult.ToArray();
}
// ReSharper disable once CoVariantArrayConversion
object[] ITableOperations.SearchRecords(string sortField, bool ascending, string searchText, StringComparison comparison) => SearchRecords(sortField, ascending, searchText, comparison);
///
/// Determines if any fields modeled with the match any of the
/// specified .
///
/// Modeled table record.
/// Values to search.
///
/// true if any fields modeled with match any of the
/// specified ; otherwise, false.
///
///
/// String comparisons will be ordinal ignoring case.
///
public bool IsSearchMatch(T record, params string[] searchValues) => IsSearchMatch(record, StringComparison.OrdinalIgnoreCase, searchValues);
bool ITableOperations.IsSearchMatch(object record, params string[] searchValues) => ((ITableOperations)this).IsSearchMatch(record, StringComparison.OrdinalIgnoreCase, searchValues);
///
/// Determines if any fields modeled with the match any of the
/// specified .
///
/// Modeled table record.
/// to use when searching string fields.
/// Values to search.
///
/// true if any fields modeled with match any of the
/// specified ; otherwise, false.
///
public bool IsSearchMatch(T record, StringComparison comparison, params string[] searchValues)
{
if ((object)s_searchTargets == null)
return false;
foreach (KeyValuePair searchTarget in s_searchTargets)
{
PropertyInfo property = searchTarget.Key;
SearchType searchType = searchTarget.Value;
Func isSearchMatch;
if (property.PropertyType == typeof(string))
{
if (searchType == SearchType.Default)
searchType = SearchType.LikeExpression;
if (searchType == SearchType.LikeExpression)
isSearchMatch = (fieldValue, searchValue) => fieldValue.IndexOf(searchValue, comparison) >= 0;
else
isSearchMatch = (fieldValue, searchValue) => fieldValue.Equals(searchValue, comparison);
}
else
{
isSearchMatch = (fieldValue, searchValue) => fieldValue.Equals(searchValue);
}
foreach (string searchValue in searchValues)
{
if (isSearchMatch(property.GetValue(record)?.ToString() ?? "", searchValue))
return true;
}
}
return false;
}
bool ITableOperations.IsSearchMatch(object value, StringComparison comparison, params string[] searchValues)
{
if (!(value is T record))
throw new ArgumentException($"Cannot execute search match for record of type \"{value?.GetType().Name ?? "null"}\", expected \"{typeof(T).Name}\"", nameof(value));
return IsSearchMatch(record, comparison, searchValues);
}
///
/// Gets the specified of records from the provided source array.
///
/// Source records array.
/// Desired page of records.
/// Desired page size.
/// A page of records.
public IEnumerable GetPageOfRecords(T[] records, int page, int pageSize) => records.ToPagedList(page, pageSize, records.Length);
IEnumerable ITableOperations.GetPageOfRecords(object[] records, int page, int pageSize)
{
try
{
return GetPageOfRecords(records.Cast().ToArray(), page, pageSize);
}
catch (InvalidCastException ex)
{
throw new ArgumentException($"One of the provided records cannot be converted to type \"{typeof(T).Name}\": {ex.Message}", nameof(records), ex);
}
}
///
/// Creates a new modeled table record queried from the specified .
///
/// Primary keys values of the record to load.
/// New modeled table record queried from the specified .
public T LoadRecord(params object[] primaryKeys)
{
try
{
return LoadRecord(Connection.RetrieveRow(m_selectRowSql, GetInterpretedPrimaryKeys(primaryKeys)));
}
catch (Exception ex)
{
InvalidOperationException opex = new InvalidOperationException($"Exception during record load for {typeof(T).Name} \"{m_selectRowSql}, {ValueList(primaryKeys)}\": {ex.Message}", ex);
if ((object)ExceptionHandler == null)
throw opex;
ExceptionHandler(opex);
return null;
}
}
object ITableOperations.LoadRecord(params object[] primaryKeys) => LoadRecord(primaryKeys);
// Cached keys are not decrypted, so any needed record interpretation steps should skip encryption
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private T LoadRecordFromCachedKeys(object[] primaryKeys, IEnumerable properties = null)
{
try
{
return LoadRecord(Connection.RetrieveRow(m_selectRowSql, GetInterpretedPrimaryKeys(primaryKeys, true)), properties ?? s_properties.Values);
}
catch (Exception ex)
{
InvalidOperationException opex = new InvalidOperationException($"Exception during record load from primary key cache for {typeof(T).Name} \"{m_selectRowSql}, {ValueList(primaryKeys)}\": {ex.Message}", ex);
if ((object)ExceptionHandler == null)
throw opex;
ExceptionHandler(opex);
return null;
}
}
///
/// Creates a new modeled table record queried from the specified .
///
/// of queried data to be loaded.
/// New modeled table record queried from the specified .
public T LoadRecord(DataRow row) => LoadRecord(row, s_properties.Values);
// This is the primary function where records are loaded from a DataRow into a modeled record of type T
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private T LoadRecord(DataRow row, IEnumerable properties)
{
try
{
T record = new T();
// Make sure record exists, if not return null instead of a blank record
if (s_hasPrimaryKeyIdentityField && GetPrimaryKeys(row).All(Common.IsDefaultValue))
return null;
foreach (PropertyInfo property in properties)
{
try
{
object value = row.ConvertField(s_fieldNames[property.Name], property.PropertyType);
if ((object)s_encryptDataTargets != null && value != null && s_encryptDataTargets.TryGetValue(property, out string keyReference))
value = value.ToString().Decrypt(keyReference, CipherStrength.Aes256);
property.SetValue(record, value, null);
}
catch (Exception ex)
{
InvalidOperationException opex = new InvalidOperationException($"Exception during record load field assignment for \"{typeof(T).Name}.{property.Name} = {row[s_fieldNames[property.Name]]}\": {ex.Message}", ex);
if ((object)ExceptionHandler == null)
throw opex;
ExceptionHandler(opex);
}
}
return record;
}
catch (Exception ex)
{
InvalidOperationException opex = new InvalidOperationException($"Exception during record load for {typeof(T).Name} from data row: {ex.Message}", ex);
if ((object)ExceptionHandler == null)
throw opex;
ExceptionHandler(opex);
return null;
}
}
object ITableOperations.LoadRecord(DataRow row) => LoadRecord(row);
///
/// Converts the given collection of into a .
///
/// The collection of records to be inserted into the data table.
/// A data table containing data from the given records.
public DataTable ToDataTable(IEnumerable records)
{
DataTable dataTable = new DataTable(s_tableName);
foreach (PropertyInfo property in s_properties.Values)
dataTable.Columns.Add(new DataColumn(s_fieldNames[property.Name]));
foreach (T record in records)
{
DataRow row = dataTable.NewRow();
foreach (PropertyInfo property in s_properties.Values)
row[s_fieldNames[property.Name]] = property.GetValue(record);
dataTable.Rows.Add(row);
}
return dataTable;
}
DataTable ITableOperations.ToDataTable(IEnumerable records)
{
try
{
return ToDataTable(records.Cast());
}
catch (InvalidCastException ex)
{
throw new ArgumentException($"One of the provided records cannot be converted to type \"{typeof(T).Name}\": {ex.Message}", nameof(records), ex);
}
}
///
/// Deletes the record referenced by the specified .
///
/// Primary keys values of the record to load.
/// Number of rows affected.
public int DeleteRecord(params object[] primaryKeys)
{
try
{
int affectedRecords = Connection.ExecuteNonQuery(m_deleteSql, GetInterpretedPrimaryKeys(primaryKeys));
if (affectedRecords > 0)
PrimaryKeyCache = null;
return affectedRecords;
}
catch (Exception ex)
{
InvalidOperationException opex = new InvalidOperationException($"Exception during record delete for {typeof(T).Name} \"{m_deleteSql}, {ValueList(primaryKeys)}\": {ex.Message}", ex);
if ((object)ExceptionHandler == null)
throw opex;
ExceptionHandler(opex);
return 0;
}
}
///
/// Deletes the specified modeled table from the database.
///
/// Record to delete.
/// Number of rows affected.
public int DeleteRecord(T record) => DeleteRecord(GetPrimaryKeys(record));
int ITableOperations.DeleteRecord(object value)
{
if (!(value is T record))
throw new ArgumentException($"Cannot delete record of type \"{value?.GetType().Name ?? "null"}\", expected \"{typeof(T).Name}\"", nameof(value));
return DeleteRecord(record);
}
///
/// Deletes the record referenced by the specified .
///
/// of queried data to be deleted.
/// Number of rows affected.
public int DeleteRecord(DataRow row) => DeleteRecord(GetPrimaryKeys(row));
///
/// Deletes the records referenced by the specified .
///
/// Record restriction to apply
///
/// Flag that determines if any existing should be applied. Defaults to
/// setting.
///
/// Number of rows affected.
///
/// If any of the parameters reference a table field that is modeled with
/// either an or , then the function
/// will need to be called, replacing the target parameter with the
/// returned value so that the field value will be properly set prior to executing the database function.
///
/// cannot be null.
public int DeleteRecord(RecordRestriction restriction, bool? applyRootQueryRestriction = null)
{
if ((object)restriction == null)
throw new ArgumentNullException(nameof(restriction));
string sqlExpression = null;
try
{
if ((object)RootQueryRestriction != null && (applyRootQueryRestriction ?? ApplyRootQueryRestrictionToDeletes))
restriction = RootQueryRestriction + restriction;
sqlExpression = $"{m_deleteWhereSql}{UpdateFieldNames(restriction.FilterExpression)}";
int affectedRecords = Connection.ExecuteNonQuery(sqlExpression, restriction.Parameters);
if (affectedRecords > 0)
PrimaryKeyCache = null;
return affectedRecords;
}
catch (Exception ex)
{
InvalidOperationException opex = new InvalidOperationException($"Exception during record delete for {typeof(T).Name} \"{sqlExpression ?? "undefined"}, {ValueList(restriction.Parameters)}\": {ex.Message}", ex);
if ((object)ExceptionHandler == null)
throw opex;
ExceptionHandler(opex);
return 0;
}
}
///
/// Deletes the records referenced by the specified SQL filter expression and parameters.
///
///
/// Filter SQL expression for restriction as a composite format string - does not include WHERE.
/// When escaping is needed for field names, use standard ANSI quotes.
///
/// Restriction parameter values.
/// Number of rows affected.
///
///
/// Each indexed parameter, e.g., "{0}", in the composite format
/// will be converted into query parameters where each of the corresponding values in the
/// collection will be applied as
/// values to an executed query.
///
///
/// If any of the specified reference a table field that is modeled with
/// either an or , then the function
/// will need to be called, replacing the target parameter with the
/// returned value so that the field value will be properly set prior to executing the database function.
///
///
/// If needed, field names that are escaped with standard ANSI quotes in the filter expression
/// will be updated to reflect what is defined in the user model.
///
///
/// This is a convenience call to .
///
///
public int DeleteRecordWhere(string filterExpression, params object[] parameters) => DeleteRecord(new RecordRestriction(filterExpression, parameters));
///
/// Updates the database with the specified modeled table ,
/// any model properties marked with will
/// be evaluated and applied before the record is provided to the data source.
///
/// Record to update.
/// Record restriction to apply, if any.
///
/// Flag that determines if any existing should be applied. Defaults to
/// setting.
///
/// Number of rows affected.
///
///
/// Record restriction is only used for custom update expressions or in cases where modeled
/// table has no defined primary keys.
///
///
/// If any of the parameters reference a table field that is modeled with
/// either an or , then the function
/// will need to be called, replacing the target parameter with the
/// returned value so that the field value will be properly set prior to executing the database function.
///
///
public int UpdateRecord(T record, RecordRestriction restriction = null, bool? applyRootQueryRestriction = null)
{
List