Как преобразовать дерево выражений в частичный SQL-запрос?
Когда EF или LINQ to SQL выполняет запрос, он:
- Строит дерево выражений из кода,
- Преобразует дерево выражений в запрос SQL,
- Выполняет запрос, получает необработанные результаты из базы данных и преобразует их в результат, который будет использоваться приложением.
Глядя на трассировку стека, я не могу понять, где происходит вторая часть.
В общем, возможно ли использовать существующую часть EF или (предпочтительно) LINQ to SQL для преобразования Expression
возражать против частичного запроса SQL (используя синтаксис Transact-SQL), или мне приходится изобретать велосипед?
Обновление: комментарий просит предоставить пример того, что я пытаюсь сделать.
Фактически, ответ Райана Райта ниже прекрасно иллюстрирует, чего я хочу достичь в результате, за исключением того факта, что мой вопрос конкретно о том, как я могу это сделать, используя существующие механизмы.NET Framework, фактически используемые в EF и LINQ to SQL, вместо того, чтобы заново изобретать колесо и писать тысячи строк не проверенного кода, чтобы сделать то же самое.
Вот также пример. Опять же, обратите внимание, что ORM-сгенерированный код отсутствует.
private class Product
{
[DatabaseMapping("ProductId")]
public int Id { get; set; }
[DatabaseMapping("Price")]
public int PriceInCents { get; set; }
}
private string Convert(Expression expression)
{
// Some magic calls to .NET Framework code happen here.
// [...]
}
private void TestConvert()
{
Expression<Func<Product, int, int, bool>> inPriceRange =
(Product product, int from, int to) =>
product.PriceInCents >= from && product.PriceInCents <= to;
string actualQueryPart = this.Convert(inPriceRange);
Assert.AreEqual("[Price] between @from and @to", actualQueryPart);
}
Где имя Price
откуда в ожидаемом запросе?
Имя может быть получено путем отражения путем запроса обычая DatabaseMapping
атрибут Price
собственностью Product
учебный класс.
Где имена @from
а также @to
откуда в ожидаемом запросе?
Эти имена являются фактическими именами параметров выражения.
Где же between … and
откуда в ожидаемом запросе?
Это возможный результат двоичного выражения. Может быть, EF или LINQ to SQL, а не between … and
Скажите, придерживайтесь [Price] >= @from and [Price] <= @to
вместо. Это тоже нормально, это не имеет значения, поскольку результат логически одинаков (я не говорю о производительности).
Почему нет where
в ожидаемом запросе?
Потому что ничего не указывает на Expression
что должно быть where
ключевое слово. Может быть, фактическое выражение - это только одно из выражений, которое позже будет объединено с бинарными операторами, чтобы построить больший запрос с предваряющим where
,
7 ответов
Короткий ответ, по-видимому, заключается в том, что вы не можете использовать часть EF или LINQ to SQL в качестве ярлыка для перевода. Вам нужен как минимум подкласс ObjectContext
добраться до internal protected
Свойство QueryProvider, и это означает все накладные расходы на создание контекста, включая все метаданные и так далее.
Предполагая, что вы в порядке с этим, чтобы получить частичный запрос SQL, например, только WHERE
В основном вам понадобится поставщик запросов и вызовите IQueryProvider.CreateQuery(), как это делает LINQ в своей реализации Queryable.Where. Чтобы получить более полный запрос, вы можете использовать ObjectQuery.ToTraceString ().
Относительно того, где это происходит, в основе провайдера LINQ обычно говорится, что
IQueryProvider возвращает ссылку на IQueryable с созданным деревом выражений, передаваемым каркасом LINQ, который используется для дальнейших вызовов. В общих чертах каждый блок запроса преобразуется в набор вызовов методов. Для каждого вызова метода используются некоторые выражения. При создании нашего провайдера - в методе IQueryProvider.CreateQuery - мы запускаем выражения и заполняем объект фильтра, который используется в методе IQueryProvider.Execute для выполнения запроса к хранилищу данных.
и это
запрос может быть выполнен двумя способами, либо путем реализации метода GetEnumerator (определенного в интерфейсе IEnumerable) в классе Query (который наследуется от IQueryable); или это может быть выполнено непосредственно средой выполнения LINQ
Проверка EF под отладчиком это первое.
Если вы не хотите полностью заново изобретать колесо и ни EF, ни LINQ to SQL не являются опциями, возможно, эта серия статей поможет:
Вот несколько источников для создания поставщика запросов, который, вероятно, потребует от вас гораздо больших усилий для реализации того, что вы хотите:
Да, это возможно, вы можете проанализировать дерево выражений LINQ, используя шаблон посетителя. Вам нужно будет создать переводчик запросов, создав подкласс ExpressionVisitor, как показано ниже. Подбирая правильные точки, вы можете использовать переводчик для построения строки SQL из выражения LINQ. Обратите внимание, что приведенный ниже код имеет дело только с основными пунктами where / orderby / skip / take, но вы можете заполнить его больше, если необходимо. Надеюсь, это послужит хорошим первым шагом.
public class MyQueryTranslator : ExpressionVisitor
{
private StringBuilder sb;
private string _orderBy = string.Empty;
private int? _skip = null;
private int? _take = null;
private string _whereClause = string.Empty;
public int? Skip
{
get
{
return _skip;
}
}
public int? Take
{
get
{
return _take;
}
}
public string OrderBy
{
get
{
return _orderBy;
}
}
public string WhereClause
{
get
{
return _whereClause;
}
}
public MyQueryTranslator()
{
}
public string Translate(Expression expression)
{
this.sb = new StringBuilder();
this.Visit(expression);
_whereClause = this.sb.ToString();
return _whereClause;
}
private static Expression StripQuotes(Expression e)
{
while (e.NodeType == ExpressionType.Quote)
{
e = ((UnaryExpression)e).Operand;
}
return e;
}
protected override Expression VisitMethodCall(MethodCallExpression m)
{
if (m.Method.DeclaringType == typeof(Queryable) && m.Method.Name == "Where")
{
this.Visit(m.Arguments[0]);
LambdaExpression lambda = (LambdaExpression)StripQuotes(m.Arguments[1]);
this.Visit(lambda.Body);
return m;
}
else if (m.Method.Name == "Take")
{
if (this.ParseTakeExpression(m))
{
Expression nextExpression = m.Arguments[0];
return this.Visit(nextExpression);
}
}
else if (m.Method.Name == "Skip")
{
if (this.ParseSkipExpression(m))
{
Expression nextExpression = m.Arguments[0];
return this.Visit(nextExpression);
}
}
else if (m.Method.Name == "OrderBy")
{
if (this.ParseOrderByExpression(m, "ASC"))
{
Expression nextExpression = m.Arguments[0];
return this.Visit(nextExpression);
}
}
else if (m.Method.Name == "OrderByDescending")
{
if (this.ParseOrderByExpression(m, "DESC"))
{
Expression nextExpression = m.Arguments[0];
return this.Visit(nextExpression);
}
}
throw new NotSupportedException(string.Format("The method '{0}' is not supported", m.Method.Name));
}
protected override Expression VisitUnary(UnaryExpression u)
{
switch (u.NodeType)
{
case ExpressionType.Not:
sb.Append(" NOT ");
this.Visit(u.Operand);
break;
case ExpressionType.Convert:
this.Visit(u.Operand);
break;
default:
throw new NotSupportedException(string.Format("The unary operator '{0}' is not supported", u.NodeType));
}
return u;
}
/// <summary>
///
/// </summary>
/// <param name="b"></param>
/// <returns></returns>
protected override Expression VisitBinary(BinaryExpression b)
{
sb.Append("(");
this.Visit(b.Left);
switch (b.NodeType)
{
case ExpressionType.And:
sb.Append(" AND ");
break;
case ExpressionType.AndAlso:
sb.Append(" AND ");
break;
case ExpressionType.Or:
sb.Append(" OR ");
break;
case ExpressionType.OrElse:
sb.Append(" OR ");
break;
case ExpressionType.Equal:
if (IsNullConstant(b.Right))
{
sb.Append(" IS ");
}
else
{
sb.Append(" = ");
}
break;
case ExpressionType.NotEqual:
if (IsNullConstant(b.Right))
{
sb.Append(" IS NOT ");
}
else
{
sb.Append(" <> ");
}
break;
case ExpressionType.LessThan:
sb.Append(" < ");
break;
case ExpressionType.LessThanOrEqual:
sb.Append(" <= ");
break;
case ExpressionType.GreaterThan:
sb.Append(" > ");
break;
case ExpressionType.GreaterThanOrEqual:
sb.Append(" >= ");
break;
default:
throw new NotSupportedException(string.Format("The binary operator '{0}' is not supported", b.NodeType));
}
this.Visit(b.Right);
sb.Append(")");
return b;
}
protected override Expression VisitConstant(ConstantExpression c)
{
IQueryable q = c.Value as IQueryable;
if (q == null && c.Value == null)
{
sb.Append("NULL");
}
else if (q == null)
{
switch (Type.GetTypeCode(c.Value.GetType()))
{
case TypeCode.Boolean:
sb.Append(((bool)c.Value) ? 1 : 0);
break;
case TypeCode.String:
sb.Append("'");
sb.Append(c.Value);
sb.Append("'");
break;
case TypeCode.DateTime:
sb.Append("'");
sb.Append(c.Value);
sb.Append("'");
break;
case TypeCode.Object:
throw new NotSupportedException(string.Format("The constant for '{0}' is not supported", c.Value));
default:
sb.Append(c.Value);
break;
}
}
return c;
}
protected override Expression VisitMember(MemberExpression m)
{
if (m.Expression != null && m.Expression.NodeType == ExpressionType.Parameter)
{
sb.Append(m.Member.Name);
return m;
}
throw new NotSupportedException(string.Format("The member '{0}' is not supported", m.Member.Name));
}
protected bool IsNullConstant(Expression exp)
{
return (exp.NodeType == ExpressionType.Constant && ((ConstantExpression)exp).Value == null);
}
private bool ParseOrderByExpression(MethodCallExpression expression, string order)
{
UnaryExpression unary = (UnaryExpression)expression.Arguments[1];
LambdaExpression lambdaExpression = (LambdaExpression)unary.Operand;
lambdaExpression = (LambdaExpression)Evaluator.PartialEval(lambdaExpression);
MemberExpression body = lambdaExpression.Body as MemberExpression;
if (body != null)
{
if (string.IsNullOrEmpty(_orderBy))
{
_orderBy = string.Format("{0} {1}", body.Member.Name, order);
}
else
{
_orderBy = string.Format("{0}, {1} {2}", _orderBy, body.Member.Name, order);
}
return true;
}
return false;
}
private bool ParseTakeExpression(MethodCallExpression expression)
{
ConstantExpression sizeExpression = (ConstantExpression)expression.Arguments[1];
int size;
if (int.TryParse(sizeExpression.Value.ToString(), out size))
{
_take = size;
return true;
}
return false;
}
private bool ParseSkipExpression(MethodCallExpression expression)
{
ConstantExpression sizeExpression = (ConstantExpression)expression.Arguments[1];
int size;
if (int.TryParse(sizeExpression.Value.ToString(), out size))
{
_skip = size;
return true;
}
return false;
}
}
Затем посетите выражение, позвонив:
var translator = new MyQueryTranslator();
string whereClause = translator.Translate(expression);
После нескольких часов поиска реализации преобразователя дерева выражений в SQL я не нашел ничего полезного, бесплатного или как-то работающего с.NET Core. Потом я нашел это. Спасибо, Райан Райт. Я взял его код и немного изменил его под свои нужды. Теперь я возвращаю его сообществу.
Текущая версия может делать следующее:
Массовое обновление
int rowCount = context
.Users
.Where(x => x.Status == UserStatus.Banned)
.Update(x => new
{
DisplayName = "Bad Guy"
});
Это приведет к следующему sql
DECLARE @p0 NVarChar
DECLARE @p1 Int
SET @p0 = 'Bad Guy'
SET @p1 = 3
UPDATE [Users]
SET [DisplayName] = @p0
WHERE ( [Status] = @p1 )
Массовое удаление
int rowCount = context
.Users
.Where(x => x.UniqueName.EndsWith("012"))
.Delete();
Произведенный sql
DECLARE @p0 NVarChar
SET @p0 = '%012'
DELETE
FROM [Users]
WHERE [UniqueName] LIKE @p0
Вывод операторов SQL
string sql = context
.Users
.Where(x => x.Status == UserStatus.LockedOut)
.OrderBy(x => x.UniqueName)
.ThenByDescending(x => x.LastLogin)
.Select(x => new
{
x.UniqueName,
x.Email
})
.ToSqlString();
Это создает sql
DECLARE @p0 Int
SET @p0 = 4
SELECT [UniqueName], [Email]
FROM [Users]
WHERE ( [Status] = @p0 )
ORDER BY [LastLogin] DESC, [UniqueName] ASC
Другой пример
string sql = context
.Users
.Where(x => x.Status == UserStatus.LockedOut)
.OrderBy(x => x.UniqueName)
.ThenByDescending(x => x.LastLogin)
.Select(x => new
{
x.UniqueName,
x.Email,
x.LastLogin
})
.Take(4)
.Skip(3)
.Distinct()
.ToSqlString();
Sql
DECLARE @p0 Int
SET @p0 = 4
SELECT DISTINCT [UniqueName], [Email], [LastLogin]
FROM [Users]
WHERE ( [Status] = @p0 )
ORDER BY [LastLogin] DESC, [UniqueName] ASC OFFSET 3 ROWS FETCH NEXT 4 ROWS ONLY
Другой пример с локальной переменной
string name ="venom";
string sql = context
.Users
.Where(x => x.LastLogin == DateTime.UtcNow && x.UniqueName.Contains(name))
.Select(x => x.Email)
.ToSqlString();
Произведенный sql
DECLARE @p0 DateTime
DECLARE @p1 NVarChar
SET @p0 = '20.06.2020 19:23:46'
SET @p1 = '%venom%'
SELECT [Email]
FROM [Users]
WHERE ( ( [LastLogin] = @p0 ) AND [UniqueName] LIKE @p1 )
Сам класс SimpleExpressionToSQL можно использовать напрямую
var simpleExpressionToSQL = new SimpleExpressionToSQL(queryable);
simpleExpressionToSQL.ExecuteNonQuery(IsolationLevel.Snapshot);
Код
Используемый здесь оценщик взят отсюда
SimpleExpressionToSQL
public class SimpleExpressionToSQL : ExpressionVisitor
{
/*
* Original By Ryan Wright: https://stackru.com/questions/7731905/how-to-convert-an-expression-tree-to-a-partial-sql-query
*/
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly List<string> _groupBy = new List<string>();
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly List<string> _orderBy = new List<string>();
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly List<SqlParameter> _parameters = new List<SqlParameter>();
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly List<string> _select = new List<string>();
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly List<string> _update = new List<string>();
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly List<string> _where = new List<string>();
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private int? _skip;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private int? _take;
public SimpleExpressionToSQL(IQueryable queryable)
{
if (queryable is null)
{
throw new ArgumentNullException(nameof(queryable));
}
Expression expression = queryable.Expression;
Visit(expression);
Type entityType = (GetEntityType(expression) as IQueryable).ElementType;
TableName = queryable.GetTableName(entityType);
DbContext = queryable.GetDbContext();
}
public string CommandText => BuildSqlStatement().Join(Environment.NewLine);
public DbContext DbContext { get; private set; }
public string From => $"FROM [{TableName}]";
public string GroupBy => _groupBy.Count == 0 ? null : "GROUP BY " + _groupBy.Join(", ");
public bool IsDelete { get; private set; } = false;
public bool IsDistinct { get; private set; }
public string OrderBy => BuildOrderByStatement().Join(" ");
public SqlParameter[] Parameters => _parameters.ToArray();
public string Select => BuildSelectStatement().Join(" ");
public int? Skip => _skip;
public string TableName { get; private set; }
public int? Take => _take;
public string Update => "SET " + _update.Join(", ");
public string Where => _where.Count == 0 ? null : "WHERE " + _where.Join(" ");
public static implicit operator string(SimpleExpressionToSQL simpleExpression) => simpleExpression.ToString();
public int ExecuteNonQuery(IsolationLevel isolationLevel = IsolationLevel.RepeatableRead)
{
DbConnection connection = DbContext.Database.GetDbConnection();
using (DbCommand command = connection.CreateCommand())
{
command.CommandText = CommandText;
command.CommandType = CommandType.Text;
command.Parameters.AddRange(Parameters);
#if DEBUG
Debug.WriteLine(ToString());
#endif
if (command.Connection.State != ConnectionState.Open)
command.Connection.Open();
using (DbTransaction transaction = connection.BeginTransaction(isolationLevel))
{
command.Transaction = transaction;
int result = command.ExecuteNonQuery();
transaction.Commit();
return result;
}
}
}
public async Task<int> ExecuteNonQueryAsync(IsolationLevel isolationLevel = IsolationLevel.RepeatableRead)
{
DbConnection connection = DbContext.Database.GetDbConnection();
using (DbCommand command = connection.CreateCommand())
{
command.CommandText = CommandText;
command.CommandType = CommandType.Text;
command.Parameters.AddRange(Parameters);
#if DEBUG
Debug.WriteLine(ToString());
#endif
if (command.Connection.State != ConnectionState.Open)
await command.Connection.OpenAsync();
using (DbTransaction transaction = connection.BeginTransaction(isolationLevel))
{
command.Transaction = transaction;
int result = await command.ExecuteNonQueryAsync();
transaction.Commit();
return result;
}
}
}
public override string ToString() =>
BuildDeclaration()
.Union(BuildSqlStatement())
.Join(Environment.NewLine);
protected override Expression VisitBinary(BinaryExpression binaryExpression)
{
_where.Add("(");
Visit(binaryExpression.Left);
switch (binaryExpression.NodeType)
{
case ExpressionType.And:
_where.Add("AND");
break;
case ExpressionType.AndAlso:
_where.Add("AND");
break;
case ExpressionType.Or:
case ExpressionType.OrElse:
_where.Add("OR");
break;
case ExpressionType.Equal:
if (IsNullConstant(binaryExpression.Right))
{
_where.Add("IS");
}
else
{
_where.Add("=");
}
break;
case ExpressionType.NotEqual:
if (IsNullConstant(binaryExpression.Right))
{
_where.Add("IS NOT");
}
else
{
_where.Add("<>");
}
break;
case ExpressionType.LessThan:
_where.Add("<");
break;
case ExpressionType.LessThanOrEqual:
_where.Add("<=");
break;
case ExpressionType.GreaterThan:
_where.Add(">");
break;
case ExpressionType.GreaterThanOrEqual:
_where.Add(">=");
break;
default:
throw new NotSupportedException(string.Format("The binary operator '{0}' is not supported", binaryExpression.NodeType));
}
Visit(binaryExpression.Right);
_where.Add(")");
return binaryExpression;
}
protected override Expression VisitConstant(ConstantExpression constantExpression)
{
switch (constantExpression.Value)
{
case null when constantExpression.Value == null:
_where.Add("NULL");
break;
default:
if (constantExpression.Type.CanConvertToSqlDbType())
{
_where.Add(CreateParameter(constantExpression.Value).ParameterName);
}
break;
}
return constantExpression;
}
protected override Expression VisitMember(MemberExpression memberExpression)
{
Expression VisitMemberLocal(Expression expression)
{
switch (expression.NodeType)
{
case ExpressionType.Parameter:
_where.Add($"[{memberExpression.Member.Name}]");
return memberExpression;
case ExpressionType.Constant:
_where.Add(CreateParameter(GetValue(memberExpression)).ParameterName);
return memberExpression;
case ExpressionType.MemberAccess:
_where.Add(CreateParameter(GetValue(memberExpression)).ParameterName);
return memberExpression;
}
throw new NotSupportedException(string.Format("The member '{0}' is not supported", memberExpression.Member.Name));
}
if (memberExpression.Expression == null)
{
return VisitMemberLocal(memberExpression);
}
return VisitMemberLocal(memberExpression.Expression);
}
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
switch (methodCallExpression.Method.Name)
{
case nameof(Queryable.Where) when methodCallExpression.Method.DeclaringType == typeof(Queryable):
Visit(methodCallExpression.Arguments[0]);
var lambda = (LambdaExpression)StripQuotes(methodCallExpression.Arguments[1]);
Visit(lambda.Body);
return methodCallExpression;
case nameof(Queryable.Select):
return ParseExpression(methodCallExpression, _select);
case nameof(Queryable.GroupBy):
return ParseExpression(methodCallExpression, _groupBy);
case nameof(Queryable.Take):
return ParseExpression(methodCallExpression, ref _take);
case nameof(Queryable.Skip):
return ParseExpression(methodCallExpression, ref _skip);
case nameof(Queryable.OrderBy):
case nameof(Queryable.ThenBy):
return ParseExpression(methodCallExpression, _orderBy, "ASC");
case nameof(Queryable.OrderByDescending):
case nameof(Queryable.ThenByDescending):
return ParseExpression(methodCallExpression, _orderBy, "DESC");
case nameof(Queryable.Distinct):
IsDistinct = true;
return Visit(methodCallExpression.Arguments[0]);
case nameof(string.StartsWith):
_where.AddRange(ParseExpression(methodCallExpression, methodCallExpression.Object));
_where.Add("LIKE");
_where.Add(CreateParameter(GetValue(methodCallExpression.Arguments[0]).ToString() + "%").ParameterName);
return methodCallExpression.Arguments[0];
case nameof(string.EndsWith):
_where.AddRange(ParseExpression(methodCallExpression, methodCallExpression.Object));
_where.Add("LIKE");
_where.Add(CreateParameter("%" + GetValue(methodCallExpression.Arguments[0]).ToString()).ParameterName);
return methodCallExpression.Arguments[0];
case nameof(string.Contains):
_where.AddRange(ParseExpression(methodCallExpression, methodCallExpression.Object));
_where.Add("LIKE");
_where.Add(CreateParameter("%" + GetValue(methodCallExpression.Arguments[0]).ToString() + "%").ParameterName);
return methodCallExpression.Arguments[0];
case nameof(Extensions.ToSqlString):
return Visit(methodCallExpression.Arguments[0]);
case nameof(Extensions.Delete):
case nameof(Extensions.DeleteAsync):
IsDelete = true;
return Visit(methodCallExpression.Arguments[0]);
case nameof(Extensions.Update):
return ParseExpression(methodCallExpression, _update);
default:
if (methodCallExpression.Object != null)
{
_where.Add(CreateParameter(GetValue(methodCallExpression)).ParameterName);
return methodCallExpression;
}
break;
}
throw new NotSupportedException($"The method '{methodCallExpression.Method.Name}' is not supported");
}
protected override Expression VisitUnary(UnaryExpression unaryExpression)
{
switch (unaryExpression.NodeType)
{
case ExpressionType.Not:
_where.Add("NOT");
Visit(unaryExpression.Operand);
break;
case ExpressionType.Convert:
Visit(unaryExpression.Operand);
break;
default:
throw new NotSupportedException($"The unary operator '{unaryExpression.NodeType}' is not supported");
}
return unaryExpression;
}
private static Expression StripQuotes(Expression expression)
{
while (expression.NodeType == ExpressionType.Quote)
{
expression = ((UnaryExpression)expression).Operand;
}
return expression;
}
[SuppressMessage("Style", "IDE0011:Add braces", Justification = "Easier to read")]
private IEnumerable<string> BuildDeclaration()
{
if (Parameters.Length == 0) /**/ yield break;
foreach (SqlParameter parameter in Parameters) /**/ yield return $"DECLARE {parameter.ParameterName} {parameter.SqlDbType}";
foreach (SqlParameter parameter in Parameters) /**/
if (parameter.SqlDbType.RequiresQuotes()) /**/ yield return $"SET {parameter.ParameterName} = '{parameter.SqlValue?.ToString().Replace("'", "''") ?? "NULL"}'";
else /**/ yield return $"SET {parameter.ParameterName} = {parameter.SqlValue}";
}
[SuppressMessage("Style", "IDE0011:Add braces", Justification = "Easier to read")]
private IEnumerable<string> BuildOrderByStatement()
{
if (Skip.HasValue && _orderBy.Count == 0) /**/ yield return "ORDER BY (SELECT NULL)";
else if (_orderBy.Count == 0) /**/ yield break;
else if (_groupBy.Count > 0 && _orderBy[0].StartsWith("[Key]")) /**/ yield return "ORDER BY " + _groupBy.Join(", ");
else /**/ yield return "ORDER BY " + _orderBy.Join(", ");
if (Skip.HasValue && Take.HasValue) /**/ yield return $"OFFSET {Skip} ROWS FETCH NEXT {Take} ROWS ONLY";
else if (Skip.HasValue && !Take.HasValue) /**/ yield return $"OFFSET {Skip} ROWS";
}
[SuppressMessage("Style", "IDE0011:Add braces", Justification = "Easier to read")]
private IEnumerable<string> BuildSelectStatement()
{
yield return "SELECT";
if (IsDistinct) /**/ yield return "DISTINCT";
if (Take.HasValue && !Skip.HasValue) /**/ yield return $"TOP ({Take.Value})";
if (_select.Count == 0 && _groupBy.Count > 0) /**/ yield return _groupBy.Select(x => $"MAX({x})").Join(", ");
else if (_select.Count == 0) /**/ yield return "*";
else /**/ yield return _select.Join(", ");
}
[SuppressMessage("Style", "IDE0011:Add braces", Justification = "Easier to read")]
private IEnumerable<string> BuildSqlStatement()
{
if (IsDelete) /**/ yield return "DELETE";
else if (_update.Count > 0) /**/ yield return $"UPDATE [{TableName}]";
else /**/ yield return Select;
if (_update.Count == 0) /**/ yield return From;
else if (_update.Count > 0) /**/ yield return Update;
if (Where != null) /**/ yield return Where;
if (GroupBy != null) /**/ yield return GroupBy;
if (OrderBy != null) /**/ yield return OrderBy;
}
private SqlParameter CreateParameter(object value)
{
string parameterName = $"@p{_parameters.Count}";
var parameter = new SqlParameter()
{
ParameterName = parameterName,
Value = value
};
_parameters.Add(parameter);
return parameter;
}
private object GetEntityType(Expression expression)
{
while (true)
{
switch (expression)
{
case ConstantExpression constantExpression:
return constantExpression.Value;
case MethodCallExpression methodCallExpression:
expression = methodCallExpression.Arguments[0];
continue;
default:
return null;
}
}
}
private IEnumerable<string> GetNewExpressionString(NewExpression newExpression, string appendString = null)
{
for (int i = 0; i < newExpression.Members.Count; i++)
{
if (newExpression.Arguments[i].NodeType == ExpressionType.MemberAccess)
{
yield return
appendString == null ?
$"[{newExpression.Members[i].Name}]" :
$"[{newExpression.Members[i].Name}] {appendString}";
}
else
{
yield return
appendString == null ?
$"[{newExpression.Members[i].Name}] = {CreateParameter(GetValue(newExpression.Arguments[i])).ParameterName}" :
$"[{newExpression.Members[i].Name}] = {CreateParameter(GetValue(newExpression.Arguments[i])).ParameterName}";
}
}
}
private object GetValue(Expression expression)
{
object GetMemberValue(MemberInfo memberInfo, object container = null)
{
switch (memberInfo)
{
case FieldInfo fieldInfo:
return fieldInfo.GetValue(container);
case PropertyInfo propertyInfo:
return propertyInfo.GetValue(container);
default: return null;
}
}
switch (expression)
{
case ConstantExpression constantExpression:
return constantExpression.Value;
case MemberExpression memberExpression when memberExpression.Expression is ConstantExpression constantExpression:
return GetMemberValue(memberExpression.Member, constantExpression.Value);
case MemberExpression memberExpression when memberExpression.Expression is null: // static
return GetMemberValue(memberExpression.Member);
case MethodCallExpression methodCallExpression:
return Expression.Lambda(methodCallExpression).Compile().DynamicInvoke();
case null:
return null;
}
throw new NotSupportedException();
}
private bool IsNullConstant(Expression expression) => expression.NodeType == ExpressionType.Constant && ((ConstantExpression)expression).Value == null;
private IEnumerable<string> ParseExpression(Expression parent, Expression body, string appendString = null)
{
switch (body)
{
case MemberExpression memberExpression:
return appendString == null ?
new string[] { $"[{memberExpression.Member.Name}]" } :
new string[] { $"[{memberExpression.Member.Name}] {appendString}" };
case NewExpression newExpression:
return GetNewExpressionString(newExpression, appendString);
case ParameterExpression parameterExpression when parent is LambdaExpression lambdaExpression && lambdaExpression.ReturnType == parameterExpression.Type:
return new string[0];
case ConstantExpression constantExpression:
return constantExpression
.Type
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Select(x => $"[{x.Name}] = {CreateParameter(x.GetValue(constantExpression.Value)).ParameterName}");
}
throw new NotSupportedException();
}
private Expression ParseExpression(MethodCallExpression expression, List<string> commandList, string appendString = null)
{
var unary = (UnaryExpression)expression.Arguments[1];
var lambdaExpression = (LambdaExpression)unary.Operand;
lambdaExpression = (LambdaExpression)Evaluator.PartialEval(lambdaExpression);
commandList.AddRange(ParseExpression(lambdaExpression, lambdaExpression.Body, appendString));
return Visit(expression.Arguments[0]);
}
private Expression ParseExpression(MethodCallExpression expression, ref int? size)
{
var sizeExpression = (ConstantExpression)expression.Arguments[1];
if (int.TryParse(sizeExpression.Value.ToString(), out int value))
{
size = value;
return Visit(expression.Arguments[0]);
}
throw new NotSupportedException();
}
}
Я опубликую расширение в комментарияхEdit: слишком долго для комментария... Я добавлю еще один ответ.
Используйте его с осторожностью в производстве
Не стесняйтесь сделать из него пакет Nuget:)
Это не полный, но вот некоторые мысли для вас, чтобы углубиться, если вы придете к этому позже:
private string CreateWhereClause(Expression<Func<T, bool>> predicate)
{
StringBuilder p = new StringBuilder(predicate.Body.ToString());
var pName = predicate.Parameters.First();
p.Replace(pName.Name + ".", "");
p.Replace("==", "=");
p.Replace("AndAlso", "and");
p.Replace("OrElse", "or");
p.Replace("\"", "\'");
return p.ToString();
}
private string AddWhereToSelectCommand(Expression<Func<T, bool>> predicate, int maxCount = 0)
{
string command = string.Format("{0} where {1}", CreateSelectCommand(maxCount), CreateWhereClause(predicate));
return command;
}
private string CreateSelectCommand(int maxCount = 0)
{
string selectMax = maxCount > 0 ? "TOP " + maxCount.ToString() + " * " : "*";
string command = string.Format("Select {0} from {1}", selectMax, _tableName);
return command;
}
В Linq2SQL вы можете использовать:
var cmd = DataContext.GetCommand(expression);
var sqlQuery = cmd.CommandText;
Вы в основном должны заново изобрести колесо. QueryProvider - это то, что выполняет перевод из деревьев выражений в собственный синтаксис хранилища. Это то, что будет обрабатывать особые ситуации, такие как string.Contains(), string.StartsWith() и все специальные функции, которые его обрабатывают. Он также будет обрабатывать поиск метаданных на различных уровнях вашего ORM (*.edml в случае Entity Framework, ориентированного на базу данных или модель). Уже есть примеры и рамки для построения команд SQL. Но то, что вы ищете, звучит как частичное решение.
Также следует понимать, что метаданные таблицы / представления необходимы для правильного определения того, что является законным. Поставщики запросов довольно сложны и выполняют за вас большую работу, помимо простых преобразований дерева выражений в SQL.
В ответ на ваш вопрос, где происходит вторая часть. Вторая часть происходит во время перечисления IQueryable. IQueryables также являются IEnumerables и, в конце концов, когда GetEnumerator вызывается, он, в свою очередь, вызывает провайдера запросов с деревом выражений, которое будет использовать свои метаданные для создания команды sql. Это не совсем то, что происходит, но это должно донести идею.
Вы можете использовать следующий код:
var query = from c in Customers
select c;
string sql = ((ObjectQuery)query).ToTraceString();
Взгляните на следующую информацию: Извлечение SQL, созданного Entity Provider.
Расширения для класса SimpleExpressionToSQL
public static class Extensions
{
private static readonly MethodInfo _deleteMethod;
private static readonly MethodInfo _deleteMethodAsync;
private static readonly MethodInfo _toSqlStringMethod;
private static readonly MethodInfo _updateMethod;
private static readonly MethodInfo _updateMethodAsync;
static Extensions()
{
Type extensionType = typeof(Extensions);
_deleteMethod = extensionType.GetMethod(nameof(Extensions.Delete), BindingFlags.Static | BindingFlags.Public);
_updateMethod = extensionType.GetMethod(nameof(Extensions.Update), BindingFlags.Static | BindingFlags.Public);
_deleteMethodAsync = extensionType.GetMethod(nameof(Extensions.DeleteAsync), BindingFlags.Static | BindingFlags.Public);
_updateMethodAsync = extensionType.GetMethod(nameof(Extensions.Update), BindingFlags.Static | BindingFlags.Public);
_toSqlStringMethod = extensionType.GetMethod(nameof(Extensions.ToSqlString), BindingFlags.Static | BindingFlags.Public);
}
public static bool CanConvertToSqlDbType(this Type type) => type.ToSqlDbTypeInternal().HasValue;
public static int Delete<T>(this IQueryable<T> queryable)
{
var simpleExpressionToSQL = new SimpleExpressionToSQL(queryable.AppendCall(_deleteMethod));
return simpleExpressionToSQL.ExecuteNonQuery();
}
public static async Task<int> DeleteAsync<T>(this IQueryable<T> queryable)
{
var simpleExpressionToSQL = new SimpleExpressionToSQL(queryable.AppendCall(_deleteMethodAsync));
return await simpleExpressionToSQL.ExecuteNonQueryAsync();
}
public static string GetTableName<TEntity>(this DbSet<TEntity> dbSet) where TEntity : class
{
DbContext context = dbSet.GetService<ICurrentDbContext>().Context;
IModel model = context.Model;
IEntityType entityTypeOfFooBar = model
.GetEntityTypes()
.First(t => t.ClrType == typeof(TEntity));
IAnnotation tableNameAnnotation = entityTypeOfFooBar.GetAnnotation("Relational:TableName");
return tableNameAnnotation.Value.ToString();
}
public static string GetTableName(this IQueryable query, Type entity)
{
QueryCompiler compiler = query.Provider.GetValueOfField<QueryCompiler>("_queryCompiler");
IModel model = compiler.GetValueOfField<IModel>("_model");
IEntityType entityTypeOfFooBar = model
.GetEntityTypes()
.First(t => t.ClrType == entity);
IAnnotation tableNameAnnotation = entityTypeOfFooBar.GetAnnotation("Relational:TableName");
return tableNameAnnotation.Value.ToString();
}
public static SqlDbType ToSqlDbType(this Type type) =>
type.ToSqlDbTypeInternal() ?? throw new InvalidCastException($"Unable to cast from '{type}' to '{typeof(DbType)}'.");
public static string ToSqlString<T>(this IQueryable<T> queryable) => new SimpleExpressionToSQL(queryable.AppendCall(_toSqlStringMethod));
public static int Update<TSource, TResult>(this IQueryable<TSource> queryable, Expression<Func<TSource, TResult>> selector)
{
var simpleExpressionToSQL = new SimpleExpressionToSQL(queryable.AppendCall(_updateMethod, selector));
return simpleExpressionToSQL.ExecuteNonQuery();
}
public static async Task<int> UpdateAsync<TSource, TResult>(this IQueryable<TSource> queryable, Expression<Func<TSource, TResult>> selector)
{
var simpleExpressionToSQL = new SimpleExpressionToSQL(queryable.AppendCall(_updateMethodAsync, selector));
return await simpleExpressionToSQL.ExecuteNonQueryAsync();
}
internal static DbContext GetDbContext(this IQueryable query)
{
QueryCompiler compiler = query.Provider.GetValueOfField<QueryCompiler>("_queryCompiler");
RelationalQueryContextFactory queryContextFactory = compiler.GetValueOfField<RelationalQueryContextFactory>("_queryContextFactory");
QueryContextDependencies dependencies = queryContextFactory.GetValueOfField<QueryContextDependencies>("_dependencies");
return dependencies.CurrentContext.Context;
}
internal static string Join(this IEnumerable<string> values, string separator) => string.Join(separator, values);
internal static bool RequiresQuotes(this SqlDbType sqlDbType)
{
switch (sqlDbType)
{
case SqlDbType.Char:
case SqlDbType.Date:
case SqlDbType.DateTime:
case SqlDbType.DateTime2:
case SqlDbType.DateTimeOffset:
case SqlDbType.NChar:
case SqlDbType.NText:
case SqlDbType.Time:
case SqlDbType.SmallDateTime:
case SqlDbType.Text:
case SqlDbType.UniqueIdentifier:
case SqlDbType.Timestamp:
case SqlDbType.VarChar:
case SqlDbType.Xml:
case SqlDbType.Variant:
case SqlDbType.NVarChar:
return true;
default:
return false;
}
}
internal static unsafe string ToCamelCase(this string value)
{
if (value == null || value.Length == 0)
{
return value;
}
string result = string.Copy(value);
fixed (char* chr = result)
{
char valueChar = *chr;
*chr = char.ToLowerInvariant(valueChar);
}
return result;
}
private static IQueryable<TResult> AppendCall<TSource, TResult>(this IQueryable<TSource> queryable, MethodInfo methodInfo, Expression<Func<TSource, TResult>> selector)
{
MethodInfo methodInfoGeneric = methodInfo.MakeGenericMethod(typeof(TSource), typeof(TResult));
MethodCallExpression methodCallExpression = Expression.Call(methodInfoGeneric, queryable.Expression, selector);
return new EntityQueryable<TResult>(queryable.Provider as IAsyncQueryProvider, methodCallExpression);
}
private static IQueryable<T> AppendCall<T>(this IQueryable<T> queryable, MethodInfo methodInfo)
{
MethodInfo methodInfoGeneric = methodInfo.MakeGenericMethod(typeof(T));
MethodCallExpression methodCallExpression = Expression.Call(methodInfoGeneric, queryable.Expression);
return new EntityQueryable<T>(queryable.Provider as IAsyncQueryProvider, methodCallExpression);
}
private static T GetValueOfField<T>(this object obj, string name)
{
FieldInfo field = obj
.GetType()
.GetField(name, BindingFlags.NonPublic | BindingFlags.Instance);
return (T)field.GetValue(obj);
}
[SuppressMessage("Style", "IDE0011:Add braces", Justification = "Easier to read than with Allman braces")]
private static SqlDbType? ToSqlDbTypeInternal(this Type type)
{
if (Nullable.GetUnderlyingType(type) is Type nullableType)
return nullableType.ToSqlDbTypeInternal();
if (type.IsEnum)
return Enum.GetUnderlyingType(type).ToSqlDbTypeInternal();
if (type == typeof(long)) /**/ return SqlDbType.BigInt;
if (type == typeof(byte[])) /**/ return SqlDbType.VarBinary;
if (type == typeof(bool)) /**/ return SqlDbType.Bit;
if (type == typeof(string)) /**/ return SqlDbType.NVarChar;
if (type == typeof(DateTime)) /**/ return SqlDbType.DateTime2;
if (type == typeof(decimal)) /**/ return SqlDbType.Decimal;
if (type == typeof(double)) /**/ return SqlDbType.Float;
if (type == typeof(int)) /**/ return SqlDbType.Int;
if (type == typeof(float)) /**/ return SqlDbType.Real;
if (type == typeof(Guid)) /**/ return SqlDbType.UniqueIdentifier;
if (type == typeof(short)) /**/ return SqlDbType.SmallInt;
if (type == typeof(object)) /**/ return SqlDbType.Variant;
if (type == typeof(DateTimeOffset)) /**/ return SqlDbType.DateTimeOffset;
if (type == typeof(TimeSpan)) /**/ return SqlDbType.Time;
if (type == typeof(byte)) /**/ return SqlDbType.TinyInt;
return null;
}
}
Не уверен, что это именно то, что вам нужно, но похоже, что это может быть близко:
string[] companies = { "Consolidated Messenger", "Alpine Ski House", "Southridge Video", "City Power & Light",
"Coho Winery", "Wide World Importers", "Graphic Design Institute", "Adventure Works",
"Humongous Insurance", "Woodgrove Bank", "Margie's Travel", "Northwind Traders",
"Blue Yonder Airlines", "Trey Research", "The Phone Company",
"Wingtip Toys", "Lucerne Publishing", "Fourth Coffee" };
// The IQueryable data to query.
IQueryable<String> queryableData = companies.AsQueryable<string>();
// Compose the expression tree that represents the parameter to the predicate.
ParameterExpression pe = Expression.Parameter(typeof(string), "company");
// ***** Where(company => (company.ToLower() == "coho winery" || company.Length > 16)) *****
// Create an expression tree that represents the expression 'company.ToLower() == "coho winery"'.
Expression left = Expression.Call(pe, typeof(string).GetMethod("ToLower", System.Type.EmptyTypes));
Expression right = Expression.Constant("coho winery");
Expression e1 = Expression.Equal(left, right);
// Create an expression tree that represents the expression 'company.Length > 16'.
left = Expression.Property(pe, typeof(string).GetProperty("Length"));
right = Expression.Constant(16, typeof(int));
Expression e2 = Expression.GreaterThan(left, right);
// Combine the expression trees to create an expression tree that represents the
// expression '(company.ToLower() == "coho winery" || company.Length > 16)'.
Expression predicateBody = Expression.OrElse(e1, e2);
// Create an expression tree that represents the expression
// 'queryableData.Where(company => (company.ToLower() == "coho winery" || company.Length > 16))'
MethodCallExpression whereCallExpression = Expression.Call(
typeof(Queryable),
"Where",
new Type[] { queryableData.ElementType },
queryableData.Expression,
Expression.Lambda<Func<string, bool>>(predicateBody, new ParameterExpression[] { pe }));
// ***** End Where *****
// ***** OrderBy(company => company) *****
// Create an expression tree that represents the expression
// 'whereCallExpression.OrderBy(company => company)'
MethodCallExpression orderByCallExpression = Expression.Call(
typeof(Queryable),
"OrderBy",
new Type[] { queryableData.ElementType, queryableData.ElementType },
whereCallExpression,
Expression.Lambda<Func<string, string>>(pe, new ParameterExpression[] { pe }));
// ***** End OrderBy *****
// Create an executable query from the expression tree.
IQueryable<string> results = queryableData.Provider.CreateQuery<string>(orderByCallExpression);
// Enumerate the results.
foreach (string company in results)
Console.WriteLine(company);