Волнующие моменты Linq to Sql

Волнующие моменты Linq to Sql

578

Я думаю, что эта статья будет увлекательна тем, кто, подобно нам, по-прежнему применять это в целом не плохо, но позабытых технологий Microsoft. Прошло наиболее года с момента моего предшествующего поста на схожую тему. За это время мы не подошли к переходу к Entity Framework (на текущий легенда, мы пойдем , когда будет стабильная версия EF 7), но у меня накопился определенный опыт, которым я желал бы поделиться.

DbType
Указав подсказки DbType (за исключением перечисления на этом ниже) не является неотклонимым для лица характеристики в Linq 2 Sql. И, естественно, Вы не должны применять неверный Тип. И в особенности не нужно так делать, раз это поле является дискриминатор, как показано в последующем примере: К примеру, раз база данных столбец имеет Тип nvarchar(50), чтоб указать, Linq 2 Sql, что столбец имеет Тип nchar(50).
[Table(Name = «directcrm.OperationSteps»)]
[InheritanceMapping(код = «», Type = typeof(OperationStep), IsDefault = true)]
// … [Column(Storage = «Тип», DbType = «nchar(50) NOT NULL», CanBeNull = false, IsDiscriminator = true)]
public string Type
{
получить
{
возвращаемый Тип;
}
набор
{
if ((type != значение))
{
SendPropertyChanging();
Тип = значение;
SendPropertyChanged();
}
}
}
} [InheritanceMapping(код = «ApplySegment», Type = typeof(ApplySegmentOperationStep))]
public class OperationStep : INotifyPropertyChanging, INotifyPropertyChanged, IValidatable
{

// Какой-то код

Отлично, давайте попробуем прочесть из базы данных типа » суть OperationStep и поглядеть, будет наследство сравнения.

Ожидалось, нет.
Свойство Тип , на 1-ый взор, содержит правильное значение, но и тип сути определяется неправильно. Попытайтесь OfType: Что ожидает наследство отображение в поле, чтоб верно показать Тип?
modelContext.Репозитории.Get<OperationStepRepository>().Элементы.OfType<ApplySegmentOperationStep>().FirstOrDefault();
И SQL, сгенерированный с помощью linq-провайдера:
DECLARE @p0 NChar = ‘ApplySegment ‘;
SELECT TOP (1) [t0].[Тип], [t0].[SegmentationSystemName], [t0].[SegmentSystemName], [t0].[Id], [t0].[Заказ], [t0].[OperationStepGroupId], [t0].[OperationStepTypeSystemName], [t0].[IsMarker]
[Directcrm].[OperationSteps] как [t0]
Где [t0].[Type] = @p0;
Будьте аккуратны. Понятно, что сейчас ошибка возникла, но, вообщем говоря, он может оставаться длительное время в «система», как лицо будет удачно сотворена и вычитаются из базы. Как долго, как Вы не замечаете странноватый хвост значений дискриминатора либо все начинает падать опосля хоть какой скрипт, который обновляет дискриминационный нрав. Значение параметра, в принципе, было ожидаемо, но найти этот баг был не чрезвычайно легкий.

Сейчас несколько слов о хранении enum в сущностей linq to sql.
— Int. Linq to sql по умолчанию (раз DbType не указан) считает, что Тип столбца Enum… Секс
{
получить { return секс; }
набор
{
if (sex != значение)
{
SendPropertyChanging();
секс = значение;
SendPropertyChanged();
}
}
}
} Соответственно для работы со последующими лица будет нереально (поле секс в таблице directcrm.Веб-сайта имеет Тип nvarchar(15)):
[Table(Name = «directcrm.Клиенты»)]
public sealed class Заказчик : INotifyPropertyChanging, INotifyPropertyChanged, IValidatable
{
// Какой-то код

[Column(Storage = «секс», CanBeNull = true)]
public Sex?
При сохранении потребителя с указанным полом, мы получаем такое выражение: При попытке вычитать из базы данных, Заказчик, лицо (в которой области секс заполнена строчка «дамский») будет падать с системой.InvalidCastException без какой-то шанс осознать, что не то, что принести.
DECLARE @p20 Int = 1

INSERT INTO [directcrm].[Клиенты](…, [Секс], …)
VALUES (…, @p7, …)
Кстати, Entity Framework не в состоянии хранить enum в строчки, так что в проекте, где мы решили применять его, пришлось применять хак: доп getter для каждого enum-поля, который был участок enum (значение enum и будет храниться в свойство типа string). Так что, раз вы держите enum строк в базе данных, используя linq to sql, не забудьте указать DbType. Любопытно, вычесть этот кортеж из таблицы просто не будет работать — будет падать все же silent System.InvalidCastException.

Тест равенства
Linq to sql в состоянии snappity в SQL как оператор ==, и вызов объекта.Equals(), но в сравнении, есть некие различия.
Так, запрос entity ActionTemplate отфильтрованного поля SystemName:
var systemName = «SystemName»;

var actionTemplate =
modelContext.Репозитории.Get<ActionTemplateRepository>()
.GetActionTemplatesIncludingNonRoot()
.FirstOrDefault(в => в.SystemName == systemName);
DECLARE @p0 NVarChar(MAX) = ‘SystemName’;

SELECT TOP (1) [t0].[Дискриминатора], [t0].[Id], [t0].[CategoryId], [t0].[Имя], [t0].[Тип], [t0].[RowVersion], [t0].[Имя_системы], [t0].[CreationCondition], [t0].[UsageDescription], [t0].[StartDateTimeUtcOverride], [t0].[EndDateTimeUtcOverride], [t0].[DateCreatedUtc], [t0].[DateChangedUtc], [t0].[CreatedById], [t0].[LastChangedById], [t0].[CampaignId], [t0].[MailingTemplateName], [t0].[UseCustomParameters], [t0].[TargetActionTemplateId], [t0].[ParentActionTemplateId], [t0].[IsTransactional], [t0].[MailingStartTime], [t0].[MailingEndTime], [t0].[IgnoreStopLists], [t0].[ReversedActionTemplateId]
[Directcrm].[ActionTemplates] как [t0]
Где [t0].[Имя_системы] = @p0
Что-нибудь необыкновенное. Но вдруг systemName будет иметь значение null?
DECLARE @p0 NVarChar(MAX) = null;

SELECT TOP (1) [t0].[Дискриминатора], [t0].[Id], [t0].[CategoryId], [t0].[Имя], [t0].[Тип], [t0].[RowVersion], [t0].[Имя_системы], [t0].[CreationCondition], [t0].[UsageDescription], [t0].[StartDateTimeUtcOverride], [t0].[EndDateTimeUtcOverride], [t0].[DateCreatedUtc], [t0].[DateChangedUtc], [t0].[CreatedById], [t0].[LastChangedById], [t0].[CampaignId], [t0].[MailingTemplateName], [t0].[UseCustomParameters], [t0].[TargetActionTemplateId], [t0].[ParentActionTemplateId], [t0].[IsTransactional], [t0].[MailingStartTime], [t0].[MailingEndTime], [t0].[IgnoreStopLists], [t0].[ReversedActionTemplateId]
[Directcrm].[ActionTemplates] как [t0]
Где [t0].[Имя_системы] = @p0
Попытайтесь объекта.равно: Разумеется, потому мы не будем ее заслуги.
строчка systemName = null;

var actionTemplate =
modelContext.Репозитории.Get<ActionTemplateRepository>()
.GetActionTemplatesIncludingNonRoot()
.FirstOrDefault(в => объект.Equals(на.SystemName systemName));
SELECT TOP (1) [t0].[Дискриминатора], [t0].[Id], [t0].[CategoryId], [t0].[Имя], [t0].[Тип], [t0].[RowVersion], [t0].[Имя_системы], [t0].[CreationCondition], [t0].[UsageDescription], [t0].[StartDateTimeUtcOverride], [t0].[EndDateTimeUtcOverride], [t0].[DateCreatedUtc], [t0].[DateChangedUtc], [t0].[CreatedById], [t0].[LastChangedById], [t0].[CampaignId], [t0].[MailingTemplateName], [t0].[UseCustomParameters], [t0].[TargetActionTemplateId], [t0].[ParentActionTemplateId], [t0].[IsTransactional], [t0].[MailingStartTime], [t0].[MailingEndTime], [t0].[IgnoreStopLists], [t0].[ReversedActionTemplateId]
[Directcrm].[ActionTemplates] как [t0]
Где 0 = 1
Раз запрос делается на столбец допускает значения null, то трансляции будут уже ждать: Это священное познание Linq to sql получил от стоимости ColumnAttribute.CanBeNull. Блестящие где 0 = 1 говорит нам, что Linq to sql не знает, что ActionTemplate.SystemName не может быть null, потому запрос так, бесполезно. К огорчению, DbType он не в состоянии осознать это.
SELECT TOP (1) [t0].[Дискриминатора], [t0].[Id], [t0].[CategoryId], [t0].[Имя], [t0].[Тип], [t0].[RowVersion], [t0].[Имя_системы], [t0].[CreationCondition], [t0].[UsageDescription], [t0].[StartDateTimeUtcOverride], [t0].[EndDateTimeUtcOverride], [t0].[DateCreatedUtc], [t0].[DateChangedUtc], [t0].[CreatedById], [t0].[LastChangedById], [t0].[CampaignId], [t0].[MailingTemplateName], [t0].[UseCustomParameters], [t0].[TargetActionTemplateId], [t0].[ParentActionTemplateId], [t0].[IsTransactional], [t0].[MailingStartTime], [t0].[MailingEndTime], [t0].[IgnoreStopLists], [t0].[ReversedActionTemplateId]
[Directcrm].[ActionTemplates] как [t0]
Где [t0].[Имя_системы] IS NULL
Так, видимо, и вы должны испытать, чтоб не применять оператор равенства, и объект.Равно, как оно переводится на наиболее «отлично».

Я знаю два метода получить left outer join:
1-ый вариант:
CustomerActions
.GroupJoin(CustomerBalanceChanges,
ca => ca,
cbch => cbch.CustomerAction,
(ca, cbchs) => cbchs
.DefaultIfEmpty()
.Выберите(cbch => new { ca, cbch }))
.SelectMany(g => g)
.Dump(); LeftOuterJoin
Как вы понимаете, Linq в целом не обеспечивает extension-способ для соединения коллекций с отсутствующими значениями в одном из их. Но время от времени при работе с linq to sql, нам необходимо зайти в sql left outer join, к примеру, и в таковых ситуациях мы используем сочетание способов, linq, которая потом переведены left outer join.
2-ой вариант:
CustomerActions
.SelectMany(ca =>
CustomerBalanceChanges
.Где(cbch => cbch.CustomerAction == ca)
.DefaultIfEmpty(),
(ca, cbch) => new { ca, cbch})
.Dump();
Оба переводятся точно так же SQL — left outer join с подзапросом, и тест-столбец (чтоб найти, существует ли суть права почти всех):
Выберите [t0].[Id], [t0].[IsTimeKnown], [t0].[BrandName], [t0].[ActionTemplateId], [t0].[CustomerId], [t0].[StaffId], [t0].[PointOfContactId], [t0].[OriginalCustomerId], [t0].[IsOriginalCustomerIdExact], [t0].[TransactionalId], [t0].[DateTimeUtc], [t0].[CreationDateTimeUtc], [t2].[тест], [t2].[Id] [Id2], [t2].[ChangeAmount], [t2].[Комменты], [t2].[CustomerActionId], [t2].[AdminSiteComments], [t2].[BalanceId]
[Directcrm].[CustomerActions] как [t0]
LEFT OUTER JOIN (
SELECT 1 AS [тест], [t1].[Id], [t1].[ChangeAmount], [t1].[Комменты], [t1].[CustomerActionId], [t1].[AdminSiteComments], [t1].[BalanceId]
[Промо].[CustomerBalanceChanges] [t1]
) [T2] на [t0].[Id] = [t2].[CustomerActionId]
Для справки: CustomerActions деяния юзера в системе, CustomerBalanceChanges — меняет баланс, по запросу, мы получаем конфигурации в балансе потребителя с подходящим действием (либо просто действий, раз это не было влияние конфигураций в балансе).
Давайте усложним запрос: «сейчас мы желаем получить не лишь поменять баланс потребителей, но и их призы:
CustomerActions
.SelectMany(ca =>
CustomerBalanceChanges
.Где(cbch => cbch.CustomerAction == ca)
.DefaultIfEmpty(),
(ca, cbch) => new { ca, cbch})
.SelectMany(cacbch =>
CustomerPrizes
.Где(cp => cacbch.ca == cp.CustomerAction)
.DefaultIfEmpty(),
(cacbch, cp) => new { cacbch.ca, cacbch.cbch, cp})
.Dump();
Выберите [t0].[Id], [t0].[IsTimeKnown], [t0].[BrandName], [t0].[ActionTemplateId], [t0].[CustomerId], [t0].[StaffId], [t0].[PointOfContactId], [t0].[OriginalCustomerId], [t0].[IsOriginalCustomerIdExact], [t0].[TransactionalId], [t0].[DateTimeUtc], [t0].[CreationDateTimeUtc], [t2].[тест], [t2].[Id] [Id2], [t2].[ChangeAmount], [t2].[Комменты], [t2].[CustomerActionId], [t2].[AdminSiteComments], [t2].[BalanceId], [t4].[test] а [test2], [t4].[Id] AS [Id3], [t4].[PrizeId], [t4].[SaleFactId], [t4].[PromoMechanicsName], [t4].[WonCustomerPrizeId], [t4].[PrizeType], [t4].[Размещено], [t4].[PromoMechanicsScheduleItemId], [t4].[CustomerActionId] [CustomerActionId2]
[Directcrm].[CustomerActions] как [t0]
LEFT OUTER JOIN (
SELECT 1 AS [тест], [t1].[Id], [t1].[ChangeAmount], [t1].[Комменты], [t1].[CustomerActionId], [t1].[AdminSiteComments], [t1].[BalanceId]
[Промо].[CustomerBalanceChanges] [t1]
) [T2] [t2].[CustomerActionId] = [t0].[Id]
LEFT OUTER JOIN (
SELECT 1 AS [тест], [t3].[Id], [t3].[PrizeId], [t3].[SaleFactId], [t3].[PromoMechanicsName], [t3].[WonCustomerPrizeId], [t3].[PrizeType], [t3].[Размещено], [t3].[PromoMechanicsScheduleItemId], [t3].[CustomerActionId]
[Промо].[CustomerPrizes] [t3]
) Как [t4] на [t0].[Id] = [t4].[CustomerActionId]
К примеру, так как мы знаем, что для каждого приза лишь изменение баланса, может быть записан последующим образом: Ничего необыкновенного, просто добавил еще один left outer join, как и ожидалось. Но вообщем говоря, мы могли бы выстроить запрос по-другому.
CustomerActions
.SelectMany(ca =>
CustomerPrizes
.Join(CustomerBalanceChanges,
cp => cp.CustomerAction,
cbch => cbch.CustomerAction,
(cp, cbch) => new { cbch, cp })
.Где(cbchcp => cbchcp.cbch.CustomerAction == ca)
.DefaultIfEmpty(),
(ca, cbchcp) => new { cbchcp.cbch, cbchcp.cp, ca})
.Dump();
Это приведет к таковой SQL:
Выберите [t2].[Id], [t2].[ChangeAmount], [t2].[Комменты], [t2].[CustomerActionId], [t2].[AdminSiteComments], [t2].[BalanceId], [t1].[Id] [Id2], [t1].[PrizeId], [t1].[SaleFactId], [t1].[PromoMechanicsName], [t1].[WonCustomerPrizeId], [t1].[PrizeType], [t1].[Размещено], [t1].[PromoMechanicsScheduleItemId], [t1].[CustomerActionId] [CustomerActionId2], [t0].[Id] AS [Id3], [t0].[IsTimeKnown], [t0].[BrandName], [t0].[ActionTemplateId], [t0].[CustomerId], [t0].[StaffId], [t0].[PointOfContactId], [t0].[OriginalCustomerId], [t0].[IsOriginalCustomerIdExact], [t0].[TransactionalId], [t0].[DateTimeUtc], [t0].[CreationDateTimeUtc]
[Directcrm].[CustomerActions] как [t0]
LEFT OUTER JOIN ([promo].[CustomerPrizes] [t1]
INNER JOIN [promo].[CustomerBalanceChanges] [t2] на [t1].[CustomerActionId] = [t2].[CustomerActionId]) [t2].[CustomerActionId] = [t0].[Id]
Способы обхода данной трудности есть? И это приводит к тому, что этот запрос не работает и выдает исключение InvalidOperationException: «значение NULL не может быть присвоено члену, который имеет Тип системы.Int32, а не NULL.». Обратите внимание, что этот SQL исчез SELECT 1 as [test] проверка существования сути. И 2-ой запрос семантически различается от первой. Во-вторых, мы могли бы сделать запрос не суть, но и собственного рода dto, вроде этого: Ну, во-первых, вы сможете перефразировать запрос как написано было в первом случае. Так как linq больше не выслеживает собственный тест-флаг, он честно пробует сделать лицо CustomerBalanceChange и CustomerPrize столбец, значение которого равно NULL, но он не сумеет писать NULL, к примеру, в CustomerBalanceChange.Id-это то, что говорит нам текст исключения…. Но это не всепригодное решение, поэтому что, кто произнес, что это можно сделать постоянно. Linq на первом сложного запроса может свалиться, и растрачивать время на перестановку join-ы совсем не охото.
CustomerActions
.SelectMany(ca =>
CustomerPrizes
.Join(CustomerBalanceChanges,
cp => cp.CustomerAction,
cbch => cbch.CustomerAction,
(cp, cbch) => new { cbch, cp })
.Где(cbchcp => cbchcp.cbch.CustomerAction == ca)
.DefaultIfEmpty(),
(ca, cbchcp) => new { cbchcp.cbch, cbchcp.cp, ca})
.Выберите(cacbchcp => new {
CustomerActionId = cacbchcp.ca.Id,
CustomerBalanceChangeId = (int?)cacbchcp.cbch.Id,
CustomerPrizeId = (int?)cacbchcp.cp.Id,
} )
Как CustomerBalanceChangeId и CustomerPrizeId сейчас nullable, не неувязка. Но мы, может быть, не нравится этот подход, поэтому что мы, может быть, будет нужно его сути (что мы желаем поменять, удалить, либо вызывать функции на их). Так что есть 3-ий путь обычный Ассоциации, которые проводят проверки на null будет на стороне sql:
null : cbchcp.cbch,
cp = cbchcp == null? CustomerActions
.SelectMany(ca =>
CustomerPrizes
.Join(CustomerBalanceChanges,
cp => cp.CustomerAction,
cbch => cbch.CustomerAction,
(cp, cbch) => new { cbch, cp })
.Где(cbchcp => cbchcp.cbch.CustomerAction == ca)
.DefaultIfEmpty(),
(ca, cbchcp) => new {
cbch = cbchcp == null? null : cbchcp.cp,
ca
})
.Dump();
Это не то что стращает sql, как это может показаться на 1-ый взор:
Выберите
(В случае
Когда [t3].[test] NULL тогда 1
По другому 0
END) AS [значение], [t3].[Id], [t3].[ChangeAmount], [t3].[Комменты], [t3].[CustomerActionId], [t3].[AdminSiteComments], [t3].[BalanceId], [t3].[Id2], [t3].[PrizeId], [t3].[SaleFactId], [t3].[PromoMechanicsName], [t3].[WonCustomerPrizeId], [t3].[PrizeType], [t3].[Размещено], [t3].[PromoMechanicsScheduleItemId], [t3].[CustomerActionId2], [t0].[Id] AS [Id3], [t0].[IsTimeKnown], [t0].[BrandName], [t0].[ActionTemplateId], [t0].[CustomerId], [t0].[StaffId], [t0].[PointOfContactId], [t0].[OriginalCustomerId], [t0].[IsOriginalCustomerIdExact], [t0].[TransactionalId], [t0].[DateTimeUtc], [t0].[CreationDateTimeUtc]
[Directcrm].[CustomerActions] как [t0]
LEFT OUTER JOIN (
SELECT 1 AS [тест], [t2].[Id], [t2].[ChangeAmount], [t2].[Комменты], [t2].[CustomerActionId], [t2].[AdminSiteComments], [t2].[BalanceId], [t1].[Id] [Id2], [t1].[PrizeId], [t1].[SaleFactId], [t1].[PromoMechanicsName], [t1].[WonCustomerPrizeId], [t1].[PrizeType], [t1].[Размещено], [t1].[PromoMechanicsScheduleItemId], [t1].[CustomerActionId] [CustomerActionId2]
[Промо].[CustomerPrizes] [t1]
INNER JOIN [promo].[CustomerBalanceChanges] [t2] на [t1].[CustomerActionId] = [t2].[CustomerActionId]
) Как [t3], [t3].[CustomerActionId] = [t0].[Id]
В этом нет ничего отвратительного до тех пор, пока запрос не стал очень огромным. Запрос был не чрезвычайно непростой, но linq to sql по-прежнему заместо того, чтоб просто применять [t3].[test] в окончательную подборку, нарисовал дизайн вариант… Но раз так, то попытайтесь объединить таблицы 10, в итоге SQL-запросов может быть несколько сотен кб! Несколько сотен кб операторов случае… Но, как вы сможете созидать, есть аспект. когда. когда.
Вот как это расширение глядит на нас: Не считая того, постоянное внедрение для обычного left outer join и хоть какой из перечисленных выше структур несколько дороже, еще проще было бы написать способ расширения LeftOuterJoin и применять его в запросах.
partialResultSelector.Оценить(outerValue) :
fullResultSelector.Оценить(outerValue, innerValue));

возвращение outerValues
.GroupJoin(innerValues, outerKeySelector, innerKeySelector, resultSelector.ExpandExpressions())
.SelectMany(result => result);
} public static IQueryable<TResult> LeftOuterJoin<TOuter, TInner, TKey, TResult>(
это IQueryable<TOuter> outerValues, IQueryable<TInner> innerValues,
Expression<Func<TOuter, TKey>> outerKeySelector,
Expression<Func<TInner, TKey>> innerKeySelector,
Expression<Func<TOuter, TInner, TResult>> fullResultSelector,
Expression<Func<TOuter, TResult>> partialResultSelector)
{
Expression<Func<TOuter, IEnumerable<TInner>, IEnumerable<TResult>> resultSelector =
(outerValue, groupedInnerValues) =>
groupedInnerValues.DefaultIfEmpty().Выберите(
innerValue => Equals(innerValue, по умолчанию(TInner))?
Предполагается последующее внедрение: Это расширение трансляции постоянно, но он употребляет проверку на null, на стороне sql.
var cbchcas = customerActions
.LeftOuterJoin(
контекста.Репозитории
.Get<CustomerBalanceChangeRepository>()
.Элементы
.Join(контекста.Репозитории
.Get<CustomerPrizeRepository>()
.Элементы
cbch => cbch.CustomerAction,
cp => cp.CustomerAction,
(cbch, cp) => new { cbch, cp }),
ca => ca,
cbchcp => cbchcp.cbch.CustomerAction,
(ca, cbchcp) => new { ca, cbchcp.cbch, cbchcp.cp },
ca => new { ca, cbch = (CustomerBalanceChange)null, cp = (CustomerPrize)null })
.ToArray();
Выберите
(В случае
Когда [t3].[test] NULL тогда 1
По другому 0
END) AS [значение], [t3].[Id], [t3].[CustomerActionId], [t3].[ChangeAmount], [t3].[Комменты], [t3].[AdminSiteComments], [t3].[BalanceId], [t3].[PrizeType], [t3].[Id2], [t3].[PrizeId], [t3].[PromoMechanicsName] [PromoMechanicsSystemName], [t3].[Размещено], [t3].[PromoMechanicsScheduleItemId], [t3].[SaleFactId], [t3].[CustomerActionId2], [t3].[WonCustomerPrizeId], [t0].[Id] AS [Id3], [t0].[DateTimeUtc], [t0].[IsTimeKnown], [t0].[PointOfContactId], [t0].[BrandName] [BrandSystemName], [t0].[CreationDateTimeUtc], [t0].[ActionTemplateId], [t0].[CustomerId], [t0].[StaffId], [t0].[OriginalCustomerId], [t0].[IsOriginalCustomerIdExact], [t0].[TransactionalId]
[Directcrm].[CustomerActions] как [t0]
LEFT OUTER JOIN (
SELECT 1 AS [тест], [t1].[Id], [t1].[CustomerActionId], [t1].[ChangeAmount], [t1].[Комменты], [t1].[AdminSiteComments], [t1].[BalanceId], [t2].[PrizeType], [t2].[Id] [Id2], [t2].[PrizeId], [t2].[PromoMechanicsName], [t2].[Размещено], [t2].[PromoMechanicsScheduleItemId], [t2].[SaleFactId], [t2].[CustomerActionId] [CustomerActionId2], [t2].[WonCustomerPrizeId]
[Промо].[CustomerBalanceChanges] [t1]
INNER JOIN [promo].[CustomerPrizes] [t2] на [t1].[CustomerActionId] = [t2].[CustomerActionId]
) Как [t3] на [t0].[Id] = [t3].[CustomerActionId]
Способ ExpandExpressions можно именовать как объекты выражение и IQueryable, который время от времени удобнее (к примеру, раз запрос происходит в пары местах). habrahabr.ru Способ ExpandExpressions рекурсивно обходит дерево выражения, на которой он был вызван рекурсивно заменяя вызовы вычислить выражение, которое было вызвано оценить. Может быть, в библиотеке будет для кого-то полезным. Вы, может быть, увидели, что в extension-способ употребляет способы оценки и ExpandExpressions. Это способы расширения из нашей библиотеки Mindbox.Выражения. Библиотека также имеет ряд увлекательных особенностей рефлексивной код.