TLDR; Есть ли способ (с использованием карты типов или другого решения) дать dynamic
результирующим наборам имя по умолчанию, такое как «(без имени столбца)» в Dapper, если имя столбца не указано?
I Я пишу редактор запросов, который позволяет пользователям писать и выполнять пользовательские запросы к базам данных MS SQL Server. Я использовал Dapper для всех наших запросов, и он прекрасно работал на 99% того, что нам нужно. Я наткнулся на загадку, и я надеюсь, что у кого-то есть решение.
Редактор запросов похож на SSMS. Я не знаю заранее, как будет выглядеть скрипт, какой будет форма или тип результирующих наборов или даже сколько результирующих наборов будет возвращено. По этой причине я пакетирую сценарии и использую Dapper's QueryMultiple
для чтения dynamic
результатов из GridReader
. Затем результаты отправляются в стороннюю таблицу данных пользовательского интерфейса (WPF). Сетка данных знает, как использовать данные Dynami c, и единственное, что ей требуется для отображения данной строки, - это как минимум одна пара значений ключа с ненулевым, но необязательно уникальным ключом и значением, допускающим значение NULL. Пока все хорошо.
Упрощенная версия вызова Dapper выглядит примерно так:
public async Task<IEnumerable<IEnumerable<T>>> QueryMultipleAsync<T>(string sql,
object parameters,
string connectionString,
CommandType commandType = CommandType.Text,
CancellationTokenSource cancellationTokenSource = null)
{
using (IDbConnection con = _dbConnectionFactory.GetConnection(connectionString))
{
con.Open();
var transaction = con.BeginTransaction();
var sqlBatches = sql
.ToUpperInvariant()
.Split(new[] { " GO ", "\r\nGO ", "\n\nGO ", "\nGO\n", "\tGO ", "\rGO "}, StringSplitOptions.RemoveEmptyEntries);
var batches = new List<CommandDefinition>();
foreach(var batch in sqlBatches)
{
batches.Add(new CommandDefinition(batch, parameters, transaction, null, commandType, CommandFlags.Buffered, cancellationTokenSource.Token));
}
var resultSet = new List<List<T>>();
foreach (var commandDefinition in batches)
{
using (GridReader reader = await con.QueryMultipleAsync(commandDefinition))
{
while (!reader.IsConsumed)
{
try
{
var result = (await reader.ReadAsync<T>()).AsList();
if (result.FirstOrDefault() is IDynamicMetaObjectProvider)
{
(result as List<dynamic>).ConvertNullKeysToNoColumnName();
}
resultSet.Add(result);
}
catch(Exception e)
{
if(e.Message.Equals("No columns were selected"))
{
break;
}
else
{
throw;
}
}
}
}
}
try
{
transaction.Commit();
}
catch (Exception ex)
{
Trace.WriteLine(ex.ToString());
if (transaction != null)
{
transaction.Rollback();
}
}
return resultSet;
}
}
public static IEnumerable<dynamic> ConvertNullKeysToNoColumnName<dynamic>(this IEnumerable<dynamic> rows)
{
foreach (var row in rows)
{
if (row is IDictionary<string, object> rowDictionary)
{
if (rowDictionary == null) continue;
rowDictionary.Where(x => string.IsNullOrEmpty(x.Key)).ToList().ForEach(x =>
{
var val = rowDictionary[x.Key];
if (x.Value == val)
{
rowDictionary.Remove(x);
rowDictionary.Add("(No Column Name)", val);
}
else
{
Trace.WriteLine("Something went wrong");
}
});
}
}
return rows;
}
Это работает с большинством запросов (и для запросов только с одним безымянным столбцом результатов), но проблема проявляется, когда пользователь пишет запрос с более чем одним безымянным столбцом, например так:
select COUNT(*), MAX(create_date) from sys.databases
.
В этом случае Dapper возвращает DapperRow, который выглядит примерно так:
{DapperRow, = '9', = '2/14/2020 9:51:54 AM'}
Таким образом, набор результатов является именно тем, что запрашивает пользователь (т. Е. Значения без имен или псевдонимов), но мне нужно предоставить (неуникальные) ключи для всех данных в сетка ...
Моей первой мыслью было просто изменить нулевые ключи в объекте DapperRow
на значение по умолчанию (например, «(без имени столбца)»), так как оно, похоже, оптимизировано для хранения. поэтому ключи таблиц хранятся в объекте только один раз (что приятно и дает хороший выигрыш в производительности для запросов с огромным набором результатов). Тип DapperRow
является приватным. После поиска я обнаружил, что могу преобразовать DapperRow
в IDictionary<string, object>
, чтобы получить доступ к ключам и значениям для объекта, и даже устанавливать и удалять значения. Вот откуда взялся метод расширения ConvertNullKeysToNoColumnName
. И это работает ... Но только один раз.
Почему? Что ж, похоже, что когда у вас есть несколько пустых или пустых ключей в DapperRow
, который приводится к IDictionary<string,object>
, и вы вызываете функцию Remove(x)
(где x - это весь элемент ИЛИ просто ключ для любого отдельного элемента с нулевой или пустой ключ), все последующие попытки разрешить другие значения с нулевым или пустым ключом через индексатор item[key]
не могут получить значение - даже если в объекте все еще существуют дополнительные пары значений ключа.
Другими словами, я не могу удалить или заменить последующие пустые ключи после удаления первого.
Я что-то упускаю из виду? Нужно ли мне просто изменить DapperRow
с помощью отражения и надеяться, что у него нет каких-либо странных побочных эффектов или что базовая структура данных не изменится позже? Или я беру производительность / память и просто копирую / отображаю весь потенциально большой набор результатов в новую последовательность, чтобы дать пустым ключам значение по умолчанию во время выполнения?