Как запросить базу данных с помощью EF Core с помощью Expression?
Я хотел бы построить динамический запрос в соответствии с метаданными и динамически запрашивать/редактировать данные, возможно, это может быть с помощью Expression. Как я мог это сделать?
Цель:
db.Blogs.AsNoTracking().FirstOrDefault(p => p.BlogId == 1);
Код?
var funcAsNoTracking = Expression.Call(dbSet.GetType().GetMethod("AsNoTracking")!);
var funcFirstOrDefault = Expression.Call(typeof(IQueryable<>).GetMethod("FirstOrDefault")!);
/* what to do next or else? */
//Code
Другой код:
public class BloggingContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; } = new();
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
Судя по тому, что вы сказали, я думаю, что вы, возможно, слишком много думаете о проблеме.
Если это так просто, как вы сказали, вы могли бы просто сделать что-то вроде.
Expression<Func<Blog, bool>> selectorExpression = (p) => p.BlogId == 1;
var blogs = db.Blogs;
Blog? result = default;
if (useAsNoTracking) // Introduce some logic that sets this.
blogs = blogs.AsNoTracking();
if (useFirstOrDefault) // Introduce some logic that sets this too.
result = blogs.FirstOrDefault(selectorExpression);
Однако все становится немного сложнее, если вы пытаетесь динамически создавать Expression
, и я ответил на аналогичный вопрос здесь C# использует строковый параметр, чтобы определить, по какому свойству фильтровать в списке объектов
Спасибо за ваш ответ @phuzi
После приятного отдыха на выходных я могу сосредоточиться на реализации и разобраться в ней сам, поделиться ею в надежде помочь другим.
Вывод: ядро EF могло бы это сделать, но я бы хотел попробовать другой способ, например LINQ to SQL, для создания динамического решения.
Обратите внимание: это всего лишь демонстрационный код, показывающий только цель, полный код большой.
Часть А: Метаданные
public class BloggingContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
public Dictionary<string, object> AllDbSets
{
get
{
//TODO: setup with dynamic registration and cache
var dic = new Dictionary<string, object>() { { nameof(Blogs), Blogs }, { nameof(Posts), Posts } };
return dic;
}
}
}
Недавно добавленные AllDbSets
Часть B: Модель динамического поиска
internal class Query
{
public Query(List<string> fileds, List<IFilter> filters)
{
this.Fileds = fileds;
this.Filters = filters;
}
public List<string> Fileds { get; set; }
public List<IFilter> Filters { get; set; }
}
internal interface IFilter
{
}
internal class EqualFilter : IFilter
{
public EqualFilter(string field, object value)
{
this.Filed = field;
this.Value = value;
}
public string Filed { get; set; }
public object Value { get; set; }
}
Примечание. Только для этой демонстрации.
Часть C. Процесс Query Engine с демонстрацией
static void Main(string[] args)
{
var query = new Query(new List<string> { "Blogs.BlogId", "Blogs.Url" }, new List<IFilter>() { new EqualFilter("Blogs.BlogId", 1) });
var queryDBSetName = query.Fileds.Select(p => p.Split(".")[0]).Distinct().First();
using BloggingContext db = new();
{
//Get DB set and basic metadata from ef core
var set = db.AllDbSets[queryDBSetName];
var setType = set.GetType();
var entityType = setType.GenericTypeArguments.First();
//AsNoTracking methodinfo
var methodInfoAsNoTracking = typeof(EntityFrameworkQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(EntityFrameworkQueryableExtensions.AsNoTracking))!.MakeGenericMethod(entityType);
//Build AsNoTracking expression
var sourceParam = Expression.Parameter(typeof(IQueryable<>).MakeGenericType(entityType), "source");
var asNoTrackingExpression = Expression.Call(methodInfoAsNoTracking, sourceParam);
//FirstOrDefault methodinfo
var methodInfoFirstOrDefault = typeof(Queryable).GetTypeInfo().DeclaredMethods.First(p => p.Name == nameof(Queryable.FirstOrDefault) && p.GetParameters().Length == 2 && p.GetParameters()[1].Name == "predicate")!.MakeGenericMethod(entityType);
//Build property predicate expression
var propertyInfo = entityType.GetProperty("BlogId")!;
var param = Expression.Parameter(entityType, "p");
var left = Expression.Property(param, propertyInfo);
var right = Expression.Constant(1, typeof(int));
var predicateExpression = Expression.Equal(left, right);
//Make lambda filter expression
Expression filterExpression = Expression.Lambda(predicateExpression, param);
//FirstOrDefault expression with filter
var callFirstOrDefault = Expression.Call(methodInfoFirstOrDefault, Expression.Lambda(asNoTrackingExpression, sourceParam).Body, filterExpression);
//Make the call of expression
var result = Expression.Lambda(callFirstOrDefault, sourceParam).Compile().DynamicInvoke(set);
}
}
Фильтр из запроса игнорируется, но для демо-версии это не имеет значения.
Опять же, это всего лишь тестовая демо-версия, над которой нужно много работать.
И даже тогда вы, вероятно, сможете решить другие проблемы, не строя дерево Expression
вручную. Просто примените это interface IKey { int Id { get; set; }}
, class Blog : IKey
, и вы сможете db.Set<T>().AsNoTracking().FirstOrDefault(p => p.Id == 1);
для любого стола.
@JeremyLakeman, насколько я понимаю, общий тип T не определен до отправки «Запроса», поскольку запрашивающая сторона попытается запросить любой объект и включить динамическую подпоследовательность. Мне не хотелось бы использовать жесткий код для переключения регистра всех типов сущностей, поскольку пример кода — лишь небольшая часть решения. система управляется метаданными и ничего не знает о фиксированных типах.
Если вам действительно нужно построить деревья выражений, сделайте это. Но ИМХО вам следует сначала попытаться избежать этого.
Мы поддерживаем .Where(), OrderBy() и т. д., но не можем настраивать статические решения.
@JeremyLakeman, недоразумение является целью системы, это система с низким кодом, управляемая метаданными, а не простое приложение, мы не могли избежать динамичных и сложных вещей.
Давайте продолжим обсуждение в чате.
Если вы можете избежать использования отражения и построения деревьев выражений вручную, вам следует это сделать. Я не говорю, что таким образом можно решить все проблемы, но большинство можно. Из вашего примера;
public interface IKey {
int Id { get; set; }
}
public class Blog : IKey
{
public int Id { get; set; }
// etc
}
public class Post
{
public int Id { get; set; }
// etc
}
public T GetItem<T>(DbContext db, int id) where T : class, IKey
=> db.Set<T>().AsNoTracking().FirstOrDefault(p => p.Id == 1);
Конечно, поддержка полностью динамических предложений является совершенно отдельной проблемой. Но для этого есть и другие существующие решения, например Dynamic Linq. Я бы не рекомендовал пытаться создать собственное решение этой проблемы.
большое спасибо за вашу демонстрацию, очень рад, что мы смогли подробно поговорить об этом разделе. В вашем сегменте кода я сталкиваюсь с реальной ситуацией: A: «Блог»/«Сообщение», определенное системным администратором, у нас не может быть общего T в коде, все, что мы знаем, это то, что у нас есть сущность и имя. B: IKey является статическим, системный администратор хотел бы иметь ключ GUID или несколько ПК. Что мы знаем, так это описание метаданных, в коде не может быть сущности/типа.
Я хотел бы использовать динамический тип объекта, ключ/значения или динамический тип времени выполнения, но это совсем другое.
Я хотел бы изучить «Dynamic Linq», чтобы посмотреть, что мы можем получить.
Для всех ключей управления вы должны иметь возможность повторно использовать интерфейс, подобный приведенному выше, даже если вам это необходимо .Property(p => p.Id).HasColumnName("BlogId")
. Вызов GetItem<T>
через .MakeGenericMethod(type).Invoke
по-прежнему намного проще, чем выполнение каждого шага этой функции посредством отражения. По крайней мере для всех тех же типов ключей. Но если абсолютно все динамично, то Entity Framework, вероятно, не тот инструмент. Если все управляется метаданными, вы можете просто использовать SqlCommand
/SqlDataReader
напрямую.
да, я доказываю, что использование EF Core — это полная ошибка. у нас уже есть библиотека AST (абстрактное синтаксическое дерево) для SQL, метаданные-> AST-> SQL, выполняемая SqlCommand/SqlDataReader и работающая с DCL, DDL, DML и DQL. Конечно, это слишком далеко за пределами возможностей. еще раз спасибо за разговор, закрываю этот вопрос.
Отдайте кесарю то, что есть кесарю. ИМХО, не пытайтесь использовать отражение со всеми этими дженериками, вместо этого напишите общий метод и используйте отражение только для его вызова. Таким образом, вы можете работать с
db.Set<T>()
,IQueryable<T>
,.AsNoTracking()
,.FirstOrDefault()
и т. д. напрямую и сосредоточиться только на построении дереваExpression
.