/// <summary> /// Gets the identity <see cref="DbField"/> object. /// </summary> /// <param name="request">The request object.</param> /// <returns>The identity <see cref="DbField"/> object.</returns> private static DbField GetIdentityField(BaseRequest request) { if (request.Type != null && request.Type != typeof(object)) { var identityProperty = IdentityCache.Get(request.Type); if (identityProperty != null) { var primaryPropery = PrimaryCache.Get(request.Type); var isPrimary = false; if (primaryPropery != null) { isPrimary = string.Equals(primaryPropery.GetUnquotedMappedName(), identityProperty.GetUnquotedMappedName(), StringComparison.OrdinalIgnoreCase); } return(new DbField(identityProperty.GetUnquotedMappedName(), isPrimary, true, false, identityProperty.PropertyInfo.PropertyType, null, null, null)); } } return(DbFieldCache.Get(request.Connection, request.Name, request.Transaction)?.FirstOrDefault(f => f.IsIdentity)); }
/// <summary> /// Gets the identity <see cref="DbField"/> object. /// </summary> /// <param name="request">The request object.</param> /// <returns>The identity <see cref="DbField"/> object.</returns> private static DbField GetIdentityField(BaseRequest request) { if (request.Type != null && request.Type != typeof(object)) { var identityProperty = IdentityCache.Get(request.Type); if (identityProperty != null) { var primaryPropery = PrimaryCache.Get(request.Type); var isPrimary = false; if (primaryPropery != null) { isPrimary = primaryPropery.GetUnquotedMappedName().ToLower() == identityProperty.GetUnquotedMappedName().ToLower(); } return(new DbField(identityProperty.GetUnquotedMappedName(), isPrimary, true, false, identityProperty.PropertyInfo.PropertyType, null, null, null)); } } return(DbFieldCache.Get(request.Connection, request.Name)?.FirstOrDefault(f => f.IsIdentity)); }
/// <summary> /// Gets a command text from the cache for the <see cref="DbConnectionExtension.InlineMerge{TEntity}(IDbConnection, object, bool?, int?, IDbTransaction, ITrace, IStatementBuilder)"/> operation. /// </summary> /// <typeparam name="TEntity">The type of the target entity.</typeparam> /// <param name="request">The request object.</param> /// <returns>The cached command text.</returns> public static string GetInlineMergeText <TEntity>(InlineMergeRequest request) where TEntity : class { var commandText = (string)null; if (m_cache.TryGetValue(request, out commandText) == false) { var primary = PrimaryKeyCache.Get <TEntity>(); var identity = IdentityCache.Get <TEntity>(); if (identity != null && identity != primary) { throw new InvalidOperationException($"Identity property must be the primary property for type '{typeof(TEntity).FullName}'."); } var isPrimaryIdentity = (identity != null); var statementBuilder = (request.StatementBuilder ?? StatementBuilderMapper.Get(request.Connection?.GetType())?.StatementBuilder ?? new SqlDbStatementBuilder()); if (statementBuilder is SqlDbStatementBuilder) { var sqlStatementBuilder = (SqlDbStatementBuilder)statementBuilder; if (isPrimaryIdentity == false) { isPrimaryIdentity = PrimaryKeyIdentityCache.Get <TEntity>(request.Connection.ConnectionString, Command.InlineMerge); } commandText = sqlStatementBuilder.CreateInlineMerge(queryBuilder: new QueryBuilder <TEntity>(), fields: request.Fields, qualifiers: request.Qualifiers, overrideIgnore: request.OverrideIgnore, isPrimaryIdentity: isPrimaryIdentity); } else { commandText = statementBuilder.CreateInlineMerge(queryBuilder: new QueryBuilder <TEntity>(), fields: request.Fields, qualifiers: request.Qualifiers, overrideIgnore: request.OverrideIgnore); } m_cache.TryAdd(request, commandText); } return(commandText); }
/// <summary> /// Set the data entities identities. /// </summary> /// <typeparam name="TEntity">The type of the data entity.</typeparam> /// <param name="entities">The list of the data entities.</param> /// <param name="identitiesResult">The result of the bulk operation.</param> private static void SetIdentities <TEntity>(IEnumerable <TEntity> entities, BulkOperationIdentitiesResult identitiesResult) where TEntity : class { // TODO: Compile this by using the FunctionCache.GetDataEntityPropertyValueSetterFunction<TEntity>(identity); // Check if there are entities if (entities?.Any() != true) { return; } // Get the results var results = identitiesResult.Identities?.OfType <object>(); if (results == null) { throw new NullReferenceException("No identities returned."); } // Get the property var property = IdentityCache.Get <TEntity>() ?? PropertyCache.Get <TEntity>(identitiesResult.IdentityPropertyName); if (property == null) { throw new PropertyNotFoundException($"Identity property not found for type '{typeof(TEntity).FullName}'."); } // Check the equality if (entities.Count() != results.Count()) { throw new InvalidOperationException("The returned identities does not matched the number of the data entities."); } // Set the identity for (var i = 0; i < entities.Count(); i++) { var entity = entities.ElementAt(i); property.PropertyInfo.SetValue(entity, results?.ElementAt(i)); } }
/// <summary> /// Inserts multiple data in the database in an asynchronous way. /// </summary> /// <typeparam name="TEntity">The type of the object (whether a data entity or a dynamic).</typeparam> /// <param name="connection">The connection object to be used.</param> /// <param name="tableName">The name of the target table to be used.</param> /// <param name="entities">The list of data entity or dynamic objects to be inserted.</param> /// <param name="batchSize">The batch size of the insertion.</param> /// <param name="fields">The mapping list of <see cref="Field"/> objects to be used.</param> /// <param name="commandTimeout">The command timeout in seconds to be used.</param> /// <param name="transaction">The transaction to be used.</param> /// <param name="trace">The trace object to be used.</param> /// <param name="statementBuilder">The statement builder object to be used.</param> /// <param name="skipIdentityCheck">True to skip the identity check.</param> /// <returns>The number of inserted rows.</returns> internal static async Task <int> InsertAllAsyncInternalBase <TEntity>(this IDbConnection connection, string tableName, IEnumerable <TEntity> entities, int batchSize = Constant.DefaultBatchOperationSize, IEnumerable <Field> fields = null, int?commandTimeout = null, IDbTransaction transaction = null, ITrace trace = null, IStatementBuilder statementBuilder = null, bool skipIdentityCheck = false) where TEntity : class { // Guard the parameters var count = GuardInsertAll(entities); // Validate the batch size batchSize = Math.Min(batchSize, count); // Get the function var callback = new Func <int, InsertAllExecutionContext <TEntity> >((int batchSizeValue) => { // Variables needed var identity = (Field)null; var dbFields = DbFieldCache.Get(connection, tableName); var inputFields = (IEnumerable <DbField>)null; var outputFields = (IEnumerable <DbField>)null; var identityDbField = dbFields?.FirstOrDefault(f => f.IsIdentity); // Set the identity value if (skipIdentityCheck == false) { identity = IdentityCache.Get <TEntity>()?.AsField(); if (identity == null && identityDbField != null) { identity = FieldCache.Get <TEntity>().FirstOrDefault(field => field.UnquotedName.ToLower() == identityDbField.UnquotedName.ToLower()); } } // Filter the actual properties for input fields inputFields = dbFields? .Where(dbField => dbField.IsIdentity == false) .Where(dbField => fields.FirstOrDefault(field => field.UnquotedName.ToLower() == dbField.UnquotedName.ToLower()) != null) .AsList(); // Set the output fields if (batchSizeValue > 1) { outputFields = identityDbField?.AsEnumerable(); } // Variables for the context var multipleEntitiesFunc = (Action <DbCommand, IList <TEntity> >)null; var identitySettersFunc = (List <Action <TEntity, DbCommand> >)null; var singleEntityFunc = (Action <DbCommand, TEntity>)null; var identitySetterFunc = (Action <TEntity, object>)null; // Get if we have not skipped it if (skipIdentityCheck == false && identity != null) { if (batchSizeValue <= 1) { identitySetterFunc = FunctionCache.GetDataEntityPropertyValueSetterFunction <TEntity>(identity); } else { identitySettersFunc = new List <Action <TEntity, DbCommand> >(); for (var index = 0; index < batchSizeValue; index++) { identitySettersFunc.Add(FunctionCache.GetDataEntityPropertySetterFromDbCommandParameterFunction <TEntity>(identity, identity.UnquotedName, index)); } } } // Identity which objects to set if (batchSizeValue <= 1) { singleEntityFunc = FunctionCache.GetDataEntityDbCommandParameterSetterFunction <TEntity>( string.Concat(typeof(TEntity).FullName, ".", tableName, ".InsertAll"), inputFields?.AsList(), null); } else { multipleEntitiesFunc = FunctionCache.GetDataEntitiesDbCommandParameterSetterFunction <TEntity>( string.Concat(typeof(TEntity).FullName, ".", tableName, ".InsertAll"), inputFields?.AsList(), outputFields, batchSizeValue); } // Identify the requests var insertAllRequest = (InsertAllRequest)null; var insertRequest = (InsertRequest)null; // Create a different kind of requests if (typeof(TEntity) == typeof(object)) { if (batchSizeValue > 1) { insertAllRequest = new InsertAllRequest(tableName, connection, fields, batchSizeValue, statementBuilder); } else { insertRequest = new InsertRequest(tableName, connection, fields, statementBuilder); } } else { if (batchSizeValue > 1) { insertAllRequest = new InsertAllRequest(typeof(TEntity), connection, fields, batchSizeValue, statementBuilder); } else { insertRequest = new InsertRequest(typeof(TEntity), connection, fields, statementBuilder); } } // Return the value return(new InsertAllExecutionContext <TEntity> { CommandText = batchSizeValue > 1 ? CommandTextCache.GetInsertAllText(insertAllRequest) : CommandTextCache.GetInsertText(insertRequest), InputFields = inputFields, OutputFields = outputFields, BatchSize = batchSizeValue, SingleDataEntityParametersSetterFunc = singleEntityFunc, MultipleDataEntitiesParametersSetterFunc = multipleEntitiesFunc, IdentityPropertySetterFunc = identitySetterFunc, IdentityPropertySettersFunc = identitySettersFunc }); }); // Get the context var context = (InsertAllExecutionContext <TEntity>)null; // Identify the number of entities (performance), get an execution context from cache context = batchSize == 1 ? InsertAllExecutionContextCache <TEntity> .Get(tableName, fields, 1, callback) : InsertAllExecutionContextCache <TEntity> .Get(tableName, fields, batchSize, callback); // Before Execution if (trace != null) { var cancellableTraceLog = new CancellableTraceLog(context.CommandText, entities, null); trace.BeforeInsertAll(cancellableTraceLog); if (cancellableTraceLog.IsCancelled) { if (cancellableTraceLog.IsThrowException) { throw new CancelledExecutionException(context.CommandText); } return(0); } context.CommandText = (cancellableTraceLog.Statement ?? context.CommandText); entities = (IEnumerable <TEntity>)(cancellableTraceLog.Parameter ?? entities); } // Before Execution Time var beforeExecutionTime = DateTime.UtcNow; // Execution variables var result = 0; // Make sure to create transaction if there is no passed one var hasTransaction = (transaction != null); try { // Ensure the connection is open await connection.EnsureOpenAsync(); if (hasTransaction == false) { // Create a transaction transaction = connection.BeginTransaction(); } // Create the command using (var command = (DbCommand)connection.CreateCommand(context.CommandText, CommandType.Text, commandTimeout, transaction)) { // Directly execute if the entities is only 1 (performance) if (context.BatchSize == 1) { foreach (var entity in entities) { // Set the values context.SingleDataEntityParametersSetterFunc(command, entity); // Actual Execution var returnValue = ObjectConverter.DbNullToNull(await command.ExecuteScalarAsync()); // Set the return value if (returnValue != null) { context.IdentityPropertySetterFunc?.Invoke(entity, returnValue); } // Iterate the result result++; } } else { foreach (var batchEntities in entities.Split(batchSize)) { var batchItems = batchEntities.AsList(); // Break if there is no more records if (batchItems.Count <= 0) { break; } // Check if the batch size has changed (probably the last batch on the enumerables) if (batchItems.Count != batchSize) { // Get a new execution context from cache context = InsertAllExecutionContextCache <TEntity> .Get(tableName, fields, batchItems.Count, callback); // Set the command properties command.CommandText = context.CommandText; // Prepare the command command.Prepare(); } // Set the values context.MultipleDataEntitiesParametersSetterFunc(command, batchItems); // Actual Execution result += await command.ExecuteNonQueryAsync(); // Set the identities if (context.IdentityPropertySettersFunc != null && command.Parameters.Count > 0) { for (var index = 0; index < batchItems.Count; index++) { var func = context.IdentityPropertySettersFunc.ElementAt(index); func(batchItems[index], command); } } } } } if (hasTransaction == false) { // Commit the transaction transaction.Commit(); } } catch { if (hasTransaction == false) { // Rollback for any exception transaction.Rollback(); } throw; } finally { if (hasTransaction == false) { // Rollback and dispose the transaction transaction.Dispose(); } } // After Execution if (trace != null) { trace.AfterInsertAll(new TraceLog(context.CommandText, entities, result, DateTime.UtcNow.Subtract(beforeExecutionTime))); } // Return the result return(result); }
/// <summary> /// Creates a SQL Statement for repository inline update operation that is meant for SQL Server. /// </summary> /// <typeparam name="TEntity"> /// The data entity object bound for the SQL Statement to be created. /// </typeparam> /// <param name="queryBuilder">An instance of query builder used to build the SQL statement.</param> /// <param name="fields">The list of the fields to be a part of inline update operation in SQL Statement composition.</param> /// <param name="where">The query expression for SQL statement.</param> /// <param name="overrideIgnore"> /// Set to true if the defined <see cref="IgnoreAttribute"/> would likely /// be ignored on the inline update operation in SQL Statement composition. /// </param> /// <returns>A string containing the composed SQL Statement for inline-update operation.</returns> public string CreateInlineUpdate <TEntity>(QueryBuilder <TEntity> queryBuilder, IEnumerable <Field> fields = null, QueryGroup where = null, bool?overrideIgnore = false) where TEntity : class { // Check for the fields presence if (fields == null) { throw new NullReferenceException("The target fields must be present."); } // Check for all the fields var properties = PropertyCache.Get <TEntity>(Command.None)? .Select(property => property.GetMappedName()); var unmatchesFields = fields?.Where(field => properties?.FirstOrDefault(property => field.Name.ToLower() == property.ToLower()) == null); if (unmatchesFields?.Count() > 0) { throw new InvalidOperationException($"The fields '{unmatchesFields.Select(field => field.AsField()).Join(", ")}' are not " + $"present at type '{typeof(TEntity).FullName}'."); } // Important fields var primary = PrimaryKeyCache.Get <TEntity>(); var identity = IdentityCache.Get <TEntity>(); if (identity != null && identity != primary) { throw new InvalidOperationException($"Identity property must be the primary property for type '{typeof(TEntity).FullName}'."); } // Variables var hasFields = fields?.Any(field => field.Name.ToLower() != primary?.GetMappedName().ToLower()) == true; // Check if there are fields if (hasFields == false) { throw new InvalidOperationException($"No inline updatable fields for object '{ClassMappedNameCache.Get<TEntity>()}'."); } // Append prefix to all parameters where?.AppendParametersPrefix(); // Check for the unmatches if (overrideIgnore == false) { var updateableFields = PropertyCache.Get <TEntity>(Command.Update) .Select(property => property.GetMappedName()); var inlineUpdateableFields = PropertyCache.Get <TEntity>(Command.InlineUpdate) .Select(property => property.GetMappedName()) .Where(field => field.ToLower() != primary?.GetMappedName().ToLower() && updateableFields.Contains(field)); var unmatchesProperties = fields?.Where(field => inlineUpdateableFields?.FirstOrDefault(property => field.Name.ToLower() == property.ToLower()) == null); if (unmatchesProperties.Count() > 0) { throw new InvalidOperationException($"The fields '{unmatchesProperties.Select(field => field.AsField()).Join(", ")}' are not " + $"inline updateable for object '{ClassMappedNameCache.Get<TEntity>()}'."); } } // Build the SQL Statement queryBuilder = queryBuilder ?? new QueryBuilder <TEntity>(); queryBuilder .Clear() .Update() .TableName() .Set() .FieldsAndParametersFrom(fields) .WhereFrom(where) .End(); // Return the query return(queryBuilder.GetString()); }
/// <summary> /// Inserts a new row in the table in an asynchronous way. /// </summary> /// <typeparam name="TEntity">The type of the object (whether a data entity or a dynamic).</typeparam> /// <typeparam name="TResult">The target type of the result.</typeparam> /// <param name="connection">The connection object to be used.</param> /// <param name="tableName">The name of the target table to be used.</param> /// <param name="entity">The data entity or dynamic object to be inserted.</param> /// <param name="fields">The mapping list of <see cref="Field"/> objects to be used.</param> /// <param name="hints">The table hints to be used.</param> /// <param name="commandTimeout">The command timeout in seconds to be used.</param> /// <param name="transaction">The transaction to be used.</param> /// <param name="trace">The trace object to be used.</param> /// <param name="statementBuilder">The statement builder object to be used.</param> /// <param name="skipIdentityCheck">True to skip the identity check.</param> /// <returns>The value of the identity field if present, otherwise, the value of the primary field.</returns> internal async static Task <TResult> InsertAsyncInternalBase <TEntity, TResult>(this IDbConnection connection, string tableName, TEntity entity, IEnumerable <Field> fields = null, string hints = null, int?commandTimeout = null, IDbTransaction transaction = null, ITrace trace = null, IStatementBuilder statementBuilder = null, bool skipIdentityCheck = false) where TEntity : class { // Variables needed var dbSetting = connection.GetDbSetting(); // Get the database fields var dbFields = await DbFieldCache.GetAsync(connection, tableName, transaction); // Get the function var callback = new Func <InsertExecutionContext <TEntity> >(() => { // Variables needed var identity = (Field)null; var inputFields = (IEnumerable <DbField>)null; var identityDbField = dbFields?.FirstOrDefault(f => f.IsIdentity); // Set the identity field if (skipIdentityCheck == false) { identity = IdentityCache.Get <TEntity>()?.AsField(); if (identity == null && identityDbField != null) { identity = FieldCache.Get <TEntity>().FirstOrDefault(field => string.Equals(field.Name.AsUnquoted(true, dbSetting), identityDbField.Name.AsUnquoted(true, dbSetting), StringComparison.OrdinalIgnoreCase)); } } // Filter the actual properties for input fields inputFields = dbFields? .Where(dbField => dbField.IsIdentity == false) .Where(dbField => fields.FirstOrDefault(field => string.Equals(field.Name.AsUnquoted(true, dbSetting), dbField.Name.AsUnquoted(true, dbSetting), StringComparison.OrdinalIgnoreCase)) != null) .AsList(); // Variables for the entity action var identityPropertySetter = (Action <TEntity, object>)null; // Get the identity setter if (skipIdentityCheck == false && identity != null) { identityPropertySetter = FunctionCache.GetDataEntityPropertyValueSetterFunction <TEntity>(identity); } // Identify the requests var insertRequest = (InsertRequest)null; // Create a different kind of requests if (typeof(TEntity) == StaticType.Object) { insertRequest = new InsertRequest(tableName, connection, transaction, fields, hints, statementBuilder); } else { insertRequest = new InsertRequest(typeof(TEntity), connection, transaction, fields, hints, statementBuilder); } // Return the value return(new InsertExecutionContext <TEntity> { CommandText = CommandTextCache.GetInsertText(insertRequest), InputFields = inputFields, ParametersSetterFunc = FunctionCache.GetDataEntityDbCommandParameterSetterFunction <TEntity>( string.Concat(typeof(TEntity).FullName, StringConstant.Period, tableName, ".Insert"), inputFields?.AsList(), null, dbSetting), IdentityPropertySetterFunc = identityPropertySetter }); }); // Get the context var context = InsertExecutionContextCache <TEntity> .Get(tableName, fields, callback); var sessionId = Guid.Empty; // Before Execution if (trace != null) { sessionId = Guid.NewGuid(); var cancellableTraceLog = new CancellableTraceLog(sessionId, context.CommandText, entity, null); trace.BeforeInsert(cancellableTraceLog); if (cancellableTraceLog.IsCancelled) { if (cancellableTraceLog.IsThrowException) { throw new CancelledExecutionException(context.CommandText); } return(default(TResult)); } context.CommandText = (cancellableTraceLog.Statement ?? context.CommandText); entity = (TEntity)(cancellableTraceLog.Parameter ?? entity); } // Before Execution Time var beforeExecutionTime = DateTime.UtcNow; // Execution variables var result = default(TResult); // Create the command using (var command = (DbCommand)(await connection.EnsureOpenAsync()).CreateCommand(context.CommandText, CommandType.Text, commandTimeout, transaction)) { // Set the values context.ParametersSetterFunc(command, entity); // Actual Execution result = Converter.ToType <TResult>(await command.ExecuteScalarAsync()); // Get explicity if needed if (Equals(result, default(TResult)) == true && dbSetting.IsMultiStatementExecutable == false) { result = Converter.ToType <TResult>(await connection.GetDbHelper().GetScopeIdentityAsync(connection, transaction)); } // Set the return value if (Equals(result, default(TResult)) == false) { context.IdentityPropertySetterFunc?.Invoke(entity, result); } } // After Execution if (trace != null) { trace.AfterInsert(new TraceLog(sessionId, context.CommandText, entity, result, DateTime.UtcNow.Subtract(beforeExecutionTime))); } // Return the result return(result); }
/// <summary> /// Selecting data from Sql with Sql IN clause usually requires 1 Parameter for every value, and this result in /// safe Sql Queries, but there is a limit of 2100 parameters on a Sql Command. This method provides a safe /// alternative implementation that is highly performant for large data sets using a list of int values (e.g Ids). /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="sqlConnection"></param> /// <param name="idList"></param> /// <param name="filterFieldName"></param> /// <param name="tableName"></param> /// <param name="fields"></param> /// <param name="orderBy"></param> /// <param name="hints"></param> /// <param name="cacheKey"></param> /// <param name="cacheItemExpiration"></param> /// <param name="commandTimeout"></param> /// <param name="transaction"></param> /// <param name="logTrace"></param> /// <param name="cancellationToken"></param> /// <param name="cache"></param> /// <returns></returns> public static async Task <IEnumerable <TEntity> > QueryBulkResultsByIdAsync <TEntity>( this SqlConnection sqlConnection, IEnumerable <int> idList, string filterFieldName = null, string tableName = null, IEnumerable <Field> fields = null, IEnumerable <OrderField> orderBy = null, string hints = null, string cacheKey = null, int?cacheItemExpiration = null, int?commandTimeout = null, IDbTransaction transaction = null, ICache cache = null, Action <string> logTrace = null, CancellationToken cancellationToken = default ) where TEntity : class { var connection = sqlConnection ?? throw new ArgumentNullException(nameof(sqlConnection)); var timer = Stopwatch.StartNew(); Field filterField; if (string.IsNullOrWhiteSpace(filterFieldName)) { //Attempt to dynamically resolve the Filter Field as the Identity or Primary Key field (if the field is a Numeric Type)! var classProp = IdentityCache.Get <TEntity>() ?? PrimaryCache.Get <TEntity>(); if (classProp == null || !classProp.PropertyInfo.PropertyType.IsNumericType()) { throw new ArgumentException( $"The filter field name was not specified and an Int Id could not be dynamically resolved from the Identity or Primary Key properties for the type [{typeof(TEntity).Name}]", nameof(filterFieldName) ); } filterField = new Field(classProp.GetMappedName()); } else { //If Specified then we use the Filter Field Name specified and attempt to resolve it on the Model! filterField = new Field(PropertyMappedNameCache.Get <TEntity>(filterFieldName) ?? filterFieldName); } var dbTableName = string.IsNullOrWhiteSpace(tableName) ? ClassMappedNameCache.Get <TEntity>() : tableName; //Ensure we have default fields; default is to include All Fields... var fieldsList = fields?.ToList(); var selectFields = fieldsList?.Any() == true ? fieldsList : FieldCache.Get <TEntity>(); //Retrieve only the select fields that are valid for the Database query! //NOTE: We guard against duplicate values as a convenience. var validSelectFields = await connection .GetValidatedDbFieldsAsync(dbTableName, selectFields.Distinct()) .ConfigureAwait(false); var dbSetting = connection.GetDbSetting(); var query = new QueryBuilder() .Clear() .Select().FieldsFrom(validSelectFields, dbSetting) .From().TableNameFrom(dbTableName, dbSetting).WriteText("data") .WriteText("INNER JOIN STRING_SPLIT(@StringSplitCsvValues, ',') split") .On().WriteText("(data.").FieldFrom(filterField).WriteText("= split.value)") .OrderByFrom(orderBy, dbSetting) .HintsFrom(hints) .End(); var commandText = query.GetString(); var commandParams = new { StringSplitCsvValues = idList.ToCsvString(false) }; logTrace?.Invoke($"Query: {commandText}"); logTrace?.Invoke($"Query Param @StringSplitCsvValues: {commandParams.StringSplitCsvValues}"); await connection.EnsureOpenAsync(cancellationToken : cancellationToken); logTrace?.Invoke($"DB Connection Established in: {timer.ToElapsedTimeDescriptiveFormat()}"); //By creating a View Model of the data we are interested in we can easily query the View // and teh complex many-to-many join is now encapsulated for us in the SQL View... var results = await connection.ExecuteQueryAsync <TEntity>( commandText, commandParams, commandType : CommandType.Text, commandTimeout : commandTimeout, transaction : transaction, cancellationToken : cancellationToken, cacheKey : cacheKey, cacheItemExpiration : cacheItemExpiration, cache : cache ).ConfigureAwait(false); logTrace?.Invoke($"Query Execution Completed in: {timer.ToElapsedTimeDescriptiveFormat()}"); return(results); }