Как сделать запрос, не имея универсального типа?

#c# #.net #linq-to-sql #dynamic-linq

#c# #.net #linq-to-sql #динамический-linq

Вопрос:

Я начну с фрагмента кода:

 var objectType = typeof(Department); // Department is entity class from linqdatacontext

using (var dataContext = new DataModel.ModelDataContext())
{
    var entity = Expression.Parameter(objectType, "model");
    var keyValue = Expression.Property(entity, "Id");
    var pkValue = Expression.Constant(reader.Value);
    var cond = Expression.Equal(keyValue, pkValue);
    var table = dataContext.GetTable(objectType);
    ... // and here i don't how to proceed
}
  

Я даже не уверен, правильно ли я строю это выражение. Однако, проще говоря, мне нужно динамически вызывать SingleOrDefault() эту таблицу, чтобы найти объект по первичному ключу. В каждом примере, который я нашел, используется общий вариант GetTable<>() , но я не могу использовать это очевидно. Я, вероятно, что-то упускаю из виду…

Комментарии:

1. Почему бы просто не сделать этот код сам по себе универсальным?

2. Ну, я этого не хочу, потому что у меня там около 30 объектов, которые мне нужно получить с помощью этого. Это было бы довольно грязно и утомительно.

3. Но если сам код является универсальным, то вам все равно нужен только один метод, если я чего-то не упускаю. (В этом случае dynamic , возможно, это решение, но я все еще не понимаю, почему здесь).

4. Я мало что знаю о dynamic ключевом слове. Где вы видите какой-либо универсальный код? У меня там только классический тип и значение первичного ключа, не более того.

5. Ах, так что в «реальной» версии параметр objectType находится в другом месте кода. Да, это остановит работу дженериков для вас, если вызывающий код не имеет соответствующего экземпляра или способен жестко запрограммировать тип. Посмотрите на ответ @SteveDanner тогда использование dynamic в Where показывает, как вызывать свойства без знания типа во время компиляции. Это будет полезно позже, хотя я думаю, что ваша проблема немного отличается.

Ответ №1:

Всякий раз, когда я создаю деревья выражений, мне нравится начинать с примера того, что я создаю:

 () => dataContext.GetTable<TEntity>().SingleOrDefault(entity => entity.Id == 1);
  

Исходя из этого, мы можем легко проанализировать целевое выражение. Вы на полпути к этому; вам просто нужно включить вызов GetTable метода в дерево выражений, а затем создать внешнее лямбда-выражение для вызова всего этого:

 using(var dataContext = new DataModel.ModelDataContext())
{
    var getTableCall = Expression.Call(
        Expression.Constant(dataContext),
        "GetTable",
        new[] { entityType });

    var entity = Expression.Parameter(entityType, "entity");

    var idCheck = Expression.Equal(
        Expression.Property(entity, "Id"),
        Expression.Constant(reader.Value));

    var idCheckLambda = Expression.Lambda(idCheck, entity);

    var singleOrDefaultCall = Expression.Call(
        typeof(Queryable),
        "SingleOrDefault",
        new[] { entityType },
        getTableCall,
        Expression.Quote(idCheckLambda));

    var singleOrDefaultLambda = Expression.Lambda<Func<object>>(
        Expression.Convert(singleOrDefaultCall, typeof(object)));

    var singleOrDefaultFunction = singleOrDefaultLambda.Compile();

    return singleOrDefaultFunction();    
}
  

Мы должны преобразовать SingleOrDefault вызов, чтобы иметь возвращаемый тип объекта, чтобы он мог служить телом Func<object> функции.

(Непроверенный)

Редактировать: параметризация контекста и значения данных

Теперь мы создаем эту функцию:

 (dataContext, value) => dataContext.GetTable<TEntity>().SingleOrDefault(entity => entity.Id == value);
  

Вы бы изменили константы на параметры и добавили эти параметры в скомпилированную вами функцию:

 var dataContextParameter = Expression.Parameter(typeof(ModelDataContext), "dataContext");

var valueParameter = Expression.Parameter(typeof(object), "value");

var getTableCall = Expression.Call(
    dataContextParameter,
    "GetTable",
    new[] { entityType });

var entity = Expression.Parameter(entityType, "entity");

var idCheck = Expression.Equal(
    Expression.Property(entity, "Id"),
    valueParameter);

var idCheckLambda = Expression.Lambda(idCheck, entity);

var singleOrDefaultCall = Expression.Call(
    typeof(Queryable),
    "SingleOrDefault",
    new[] { entityType },
    getTableCall,
    Expression.Quote(idCheckLambda));

var singleOrDefaultLambda =
    Expression.Lambda<Func<ModelDataContext, object, object>>(
        Expression.Convert(singleOrDefaultCall, typeof(object)),
        dataContextParameter,
        valueParameter);

var singleOrDefaultFunction = singleOrDefaultLambda.Compile();

// Usage

using(var dataContext = new DataModel.ModelDataContext())
{
    return singleOrDefaultFunction(dataContext, reader.Value);    
}
  

Комментарии:

1. Вау, это выглядит отвратительно 🙂 Я обязательно попробую это позже, сейчас мне нужно выполнить некоторые поручения. Я дам вам знать, если бы это сработало. Спасибо.

2. Хорошо, все работает нормально, большое вам спасибо. Теперь я думаю о некоторой оптимизации, потому что для этого будет много вызовов, и компиляция этого каждый раз может пойти не так. И есть также проблема с тем, что dataContext он удаляется в конце использования, поэтому его нельзя использовать как константу. Я пытаюсь что-то Expression.Variable сделать, но не уверен, как правильно это использовать. Было бы здорово иметь предварительно скомпилированную функцию, в которую можно поместить параметры с DataContext и Value, а затем выполнить ее для получения результата.

3. Ты гений 🙂 Спасибо, это именно то, что мне нужно. Хотя это плохо работает, Expression.Equal потому Id что свойство — это тип Int32 , и я передаю object его. Но это не имеет большого значения, я могу с этим справиться, в данном случае это достаточно хорошо.

Ответ №2:

Если вы используете .NET 4, вы можете попробовать преобразовать возвращаемые объекты в as dynamic , чтобы затем можно было запрашивать следующим образом.

 using (var dataContext = new DataModel.ModelDataContext())
{
   var entity = Expression.Parameter(objectType, "model");
   var keyValue = Expression.Property(entity, "Id");
   var pkValue = Expression.Constant(reader.Value);
   var cond = Expression.Equal(keyValue, pkValue);
   var table = dataContext.GetTable(objectType);

   var result = table.Where(ent => ((dynamic)ent).SomeField == "SomeValue");
}
  

Комментарии:

1. Да, я использую .NET 4. Это было бы неплохо, но проблема начинается с табличной переменной. Нет метода Where, потому что он не типизирован или по какой-то другой причине.

Ответ №3:

Я все еще не совсем уверен в отношении всей вашей проблемы (и я подозреваю, что ответ about dynamic решит часть того, что тоже возникнет). Тем не менее, просто для ответа:

В каждом примере, который я нашел, используется общий вариант GetTable<>() , но я не могу использовать это, очевидно

Для любого T , Table<T> реализует (среди других интерфейсов) ITable<T> и ITable . Первый типизирован в общем виде, второй — нет.

Форма GetTable<T>() возвращает такой Table<T> . Однако форма GetTable(Type t) возвращает ITable . Поскольку ITable наследуется от IQueryable вас, вы можете запросить его. Если вам нужно что-то сделать с этим запросом, который обычно требует знания типа (например, сравнения по заданному свойству), то dynamic , согласно предыдущему ответу, данному Стивом Даннером, это позволяет сделать.

Комментарии:

1. Хорошо, но я все еще, вероятно, что-то упускаю в этом. Как я могу запросить IQueryable , не зная там типа? Когда я использую универсальный GetTable<T> , я получаю возможные методы, такие как Where и Select . Но с не-общим GetTable(Type t) нет ничего. Я не знаю, как я мог бы использовать dynamic там (в основном потому, что это ключевое слово все еще неизвестно для меня )

Ответ №4:

Лично я бы сделал это, используя отражение и библиотеку динамических запросов LINQ.

Вы можете получить список всех ключей для таблицы с dataContext.Mapping.GetMetaType(objectType).IdentityMembers помощью, а затем получить доступ к данным с помощью чего-то вроде dataContext.GetTable(objectType).Where(key.Name "==@0", id) .

Очевидно, я пропустил несколько шагов — если у вас несколько ключей, вам нужно будет создать более полный предикат с циклом .IdentityMembers , и если у вас всегда есть только один ключ, вы можете использовать .Сначала() на нем. Я тоже его не тестировал, но он должен быть довольно близким. Вероятно, всего будет 6-7 строк кода — я могу написать его (и протестировать), если вам это нужно.


Редактировать: библиотека динамических запросов LINQ может быть загружена из Microsoft по адресу http://msdn.microsoft.com/en-us/vcsharp/bb894665.aspx — просто включите DynamicLinq.cs в свой проект, и все в порядке.

Комментарии:

1. Все та же проблема. Не универсальная версия GetTable does не имеет Where метода.

2. @FredyC: Я забыл, что установил отдельную библиотеку динамических запросов LINQ. Упс. Добавлена ссылка на него.