Цікаві моменти роботи Linq to Sql

Минуло вже більше року з мого попереднього поста на схожу тему. За цей час ми не наблизилися до переходу на Entity Framework (за поточною легендою, ми перейдемо, коли з'явиться стабільна версія EF 7), ну а у мене накопичилося деяку кількість досвіду, яким я б хотів поділитися. Думаю, що ця стаття буде цікава тим, хто, як і ми, до цих пір користуються цією загалом непоганий, але забутою Microsoft технологією.

DbType

Вказівка підказки DbType (за винятком enum'ів, про це нижче) не є обов'язковим для властивостей сутностей в Linq 2 Sql. І вже точно не варто вказувати неправильний DbType. Наприклад, не варто, якщо в базі колонка має тип nvarchar(50), вказувати Linq 2 Sql, що колонка має тип nchar(50). І особливо не варто так робити, якщо це поле є дискримінатором, як у наступному прикладі:

[Table(Name = "directcrm.OperationSteps")]
[InheritanceMapping(Code = "", Type = typeof(OperationStep), IsDefault = true)]
// ...
[InheritanceMapping(Code = "ApplySegment", Type = typeof(ApplySegmentOperationStep))]
public class OperationStep : INotifyPropertyChanging, INotifyPropertyChanged, IValidatable
{

// Деяка кількість коду
...

[Column(Storage = "type", DbType = "nchar(50) NOT NULL", CanBeNull = false, IsDiscriminator = true)]
public string Type
{
get
{
return type;
}
set
{
if ((type != value))
{
SendPropertyChanging();
type = value;
SendPropertyChanged();
}
}
}
}


Ну що ж, спробуємо прочитати з бази сутність типу OperationStep і подивитися, чи впорається Inheritance Mapping.

image
Очікувано, немає.

Властивість Type на перший погляд містить вірне значення, але тип сутності визначено неправильно. Що ж очікує Inharitance Mapping побачити в полі для того, щоб правильно зіставити тип? Спробуємо OfType:

modelContext.Repositories.Get<OperationStepRepository>().Items.OfType<ApplySegmentOperationStep>().FirstOrDefault();


І SQL, згенерований linq-провайдером:

DECLARE @p0 NChar = 'ApplySegment ';
SELECT TOP (1) [t0].[Type], [t0].[SegmentationSystemName], [t0].[SegmentSystemName], [t0].[Id], [t0].[Order], [t0].[OperationStepGroupId], [t0].[OperationStepTypeSystemName], [t0].[IsMarker]
FROM [directcrm].[OperationSteps] AS [t0]
WHERE [t0].[Type] = @p0;


Значення параметра, в принципі, було очікувано, але виявити такий баг було не дуже просто. Будьте уважнішими. Ясна річ, що зараз баг проявився, але взагалі кажучи, він може залишатися тривалий час у системі, так як сутності будуть правильно створюватися і вычитываться з бази. До тих пір, поки ви не звернете увагу на дивний хвіст значень дискримінатора або все не почне валитися після якого-небудь скрипта, оновлювального дискриминаторы.

Тепер пара слів про зберігання enum'ів в сутності linq to sql.

Linq to sql за замовчуванням (якщо DbType не вказано) вважає, що тип колонки у Enum'а — Int. Відповідно працювати з наступного сутністю буде неможливо (поле Sex у таблиці directcrm.Customers тип nvarchar(15)):
[Table(Name = "directcrm.Customers")]
public sealed class Customer : INotifyPropertyChanging, INotifyPropertyChanged, IValidatable
{
// Деяка кількість коду 

[Column(Storage = "sex", CanBeNull = true)]
public Sex? Sex
{
get { return sex; }
set
{
if (sex != value)
{
SendPropertyChanging();
sex = value;
SendPropertyChanged();
}
}
}
}


При спробі віднімати з бази сутність Customer (в якій поле Sex заповнено рядок «female») буде падати з System.InvalidCastException без якихось шансів зрозуміти, що саме не вдалося до чого привести. При збереженні споживача з зазначеним підлогою ми отримаємо ось такий запит:

DECLARE @p20 Int = 1

INSERT INTO [directcrm].[Customers](..., [Sex], ...)
VALUES (..., @p7, ...)


Що примітно, віднімати такий кортеж з таблиці так само не вийде — впаде все той же мовчазний System.InvalidCastException. Так що якщо зберігайте enum'и рядками в базі, використовуючи linq to sql, не забувайте вказувати DbType.
До слова сказати, Entity Framework не в змозі зберігати enum'и в рядках, тому в проекті, де ми вирішили його використати, довелося використовувати хак: додатковий getter для кожного enum-поля, який сам парсил enum (при цьому значення enum'а передбачається зберігати властивості типу string).

Перевірка на рівність

Linq to sql в стані смаппить в SQL оператор ==, так і виклик object.Equals(), проте в маппинге спостерігаються деякі відмінності.

Отже, запит сутності ActionTemplate з фільтрацією по полю SystemName:
var systemName = "SystemName";

var actionTemplate =
modelContext.Repositories.Get<ActionTemplateRepository>()
.GetActionTemplatesIncludingNonRoot()
.FirstOrDefault(at => at.SystemName == systemName);


DECLARE @p0 NVarChar(MAX) = 'SystemName';

SELECT TOP (1) [t0].[Discriminator], [t0].[Id], [t0].[CategoryId], [t0].[Name], [t0].[Type], [t0].[RowVersion], [t0].[SystemName], [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]
FROM [directcrm].[ActionTemplates] AS [t0]
WHERE [t0].[SystemName] = @p0


Нічого незвичайного. Але раптом systemName буде мати значення null?

DECLARE @p0 NVarChar(MAX) = null;

SELECT TOP (1) [t0].[Discriminator], [t0].[Id], [t0].[CategoryId], [t0].[Name], [t0].[Type], [t0].[RowVersion], [t0].[SystemName], [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]
FROM [directcrm].[ActionTemplates] AS [t0]
WHERE [t0].[SystemName] = @p0


Ясна річ, таким чином ми нічого доброго не доб'ємося. Спробуємо object.equals:

string systemName = null;

var actionTemplate =
modelContext.Repositories.Get<ActionTemplateRepository>()
.GetActionTemplatesIncludingNonRoot()
.FirstOrDefault(at => object.Equals(at.SystemName, systemName));


SELECT TOP (1) [t0].[Discriminator], [t0].[Id], [t0].[CategoryId], [t0].[Name], [t0].[Type], [t0].[RowVersion], [t0].[SystemName], [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]
FROM [directcrm].[ActionTemplates] AS [t0]
WHERE 0 = 1


Геніальне
WHERE 0 = 1
підказує нам, що Linq to sql знає, що ActionTemplate.SystemName не може бути null, тому і запит такий марний. Це сакральне знання Linq to sql отримав значення ColumnAttribute.CanBeNull. На жаль, DbType він не вміє це розуміти.
Якщо ж запит робиться по колонці, допускає відсутність значення, то трансляція буде вже очікуваної:

SELECT TOP (1) [t0].[Discriminator], [t0].[Id], [t0].[CategoryId], [t0].[Name], [t0].[Type], [t0].[RowVersion], [t0].[SystemName], [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]
FROM [directcrm].[ActionTemplates] AS [t0]
WHERE [t0].[SystemName] IS NULL


Тому мабуть потрібно намагатися використовувати не оператор рівності, а object.Equals, так як він транслюється більш «якісно».

LeftOuterJoin

Як відомо, Linq взагалі не надає extension-методу для з'єднання колекцій з можливістю відсутності значення в одній з них. Але іноді при роботі з linq to sql нам потрібно отримати в sql left outer join, приміром, і в таких ситуаціях ми використовуємо комбінації методів linq, які в підсумку транслюються в left outer join. Мені відомі два способи отримати left outer join:
Перший варіант:
CustomerActions
.GroupJoin(CustomerBalanceChanges,
ca => ca,
cbch => cbch.CustomerAction,
(ca, cbchs) => cbchs
.DefaultIfEmpty()
.Select(cbch => new { ca, cbch }))
.SelectMany(g => g)
.Dump();


Другий варіант:
CustomerActions
.SelectMany(ca => 
CustomerBalanceChanges
.Where(cbch => cbch.CustomerAction == ca)
.DefaultIfEmpty(),
(ca, cbch) => new { ca, cbch})
.Dump();


Обидва варіанти транслюються в абсолютно ідентичний SQL — left outer join з підзапит і test-колонкою (для визначення, чи є сутність з правого множини):

SELECT [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].[test], [t2].[Id] AS [Id2], [t2].[ChangeAmount], [t2].[Comments], [t2].[CustomerActionId], [t2].[AdminSiteComments], [t2].[BalanceId]
FROM [directcrm].[CustomerActions] AS [t0]
LEFT OUTER JOIN (
SELECT 1 AS [test], [t1].[Id], [t1].[ChangeAmount], [t1].[Comments], [t1].[CustomerActionId], [t1].[AdminSiteComments], [t1].[BalanceId]
FROM [promo].[CustomerBalanceChanges] AS [t1]
) AS [t2] ON [t0].[Id] = [t2].[CustomerActionId]


Для довідки: CustomerActions — дії споживача в системі, CustomerBalanceChanges — його зміни балансу запитом ми отримуємо зміни балансу споживача з відповідними діями (або просто дію, якщо це було не дію зміни балансу).

Ускладнимо запит: тепер ми хочемо отримувати не тільки зміни балансу споживачів, але ще і їх призи:
CustomerActions
.SelectMany(ca => 
CustomerBalanceChanges
.Where(cbch => cbch.CustomerAction == ca)
.DefaultIfEmpty(),
(ca, cbch) => new { ca, cbch})
.SelectMany(cacbch => 
CustomerPrizes
.Where(cp => cacbch.ca == cp.CustomerAction)
.DefaultIfEmpty(),
(cacbch, cp) => new { cacbch.ca, cacbch.cbch, cp})
.Dump();


SELECT [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].[test], [t2].[Id] AS [Id2], [t2].[ChangeAmount], [t2].[Comments], [t2].[CustomerActionId], [t2].[AdminSiteComments], [t2].[BalanceId], [t4].[test] AS [test2], [t4].[Id] AS [Id3], [t4].[PrizeId], [t4].[SaleFactId], [t4].[PromoMechanicsName], [t4].[WonCustomerPrizeId], [t4].[PrizeType], [t4].[Published], [t4].[PromoMechanicsScheduleItemId], [t4].[CustomerActionId] AS [CustomerActionId2]
FROM [directcrm].[CustomerActions] AS [t0]
LEFT OUTER JOIN (
SELECT 1 AS [test], [t1].[Id], [t1].[ChangeAmount], [t1].[Comments], [t1].[CustomerActionId], [t1].[AdminSiteComments], [t1].[BalanceId]
FROM [promo].[CustomerBalanceChanges] AS [t1]
) AS [t2] ON [t2].[CustomerActionId] = [t0].[Id]
LEFT OUTER JOIN (
SELECT 1 AS [test], [t3].[Id], [t3].[PrizeId], [t3].[SaleFactId], [t3].[PromoMechanicsName], [t3].[WonCustomerPrizeId], [t3].[PrizeType], [t3].[Published], [t3].[PromoMechanicsScheduleItemId], [t3].[CustomerActionId]
FROM [promo].[CustomerPrizes] AS [t3]
) AS [t4] ON [t0].[Id] = [t4].[CustomerActionId]


Нічого незвичайного, просто додався ще один left outer join, як і очікувалося. Але взагалі кажучи, ми могли б побудувати запит і по-іншому. Наприклад, ми знаємо, що для кожного призу точно є зміна балансу, можна було б написати ось так:

CustomerActions
.SelectMany(ca => 
CustomerPrizes
.Join(CustomerBalanceChanges,
cp => cp.CustomerAction,
cbch => cbch.CustomerAction,
(cp, cbch) => new { cbch, cp })
.Where(cbchcp => cbchcp.cbch.CustomerAction == ca)
.DefaultIfEmpty(),
(ca, cbchcp) => new { cbchcp.cbch, cbchcp.cp, ca})
.Dump();


Це призведе до ось такого SQL:

SELECT [t2].[Id], [t2].[ChangeAmount], [t2].[Comments], [t2].[CustomerActionId], [t2].[AdminSiteComments], [t2].[BalanceId], [t1].[Id] AS [Id2], [t1].[PrizeId], [t1].[SaleFactId], [t1].[PromoMechanicsName], [t1].[WonCustomerPrizeId], [t1].[PrizeType], [t1].[Published], [t1].[PromoMechanicsScheduleItemId], [t1].[CustomerActionId] AS [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]
FROM [directcrm].[CustomerActions] AS [t0]
LEFT OUTER JOIN ([promo].[CustomerPrizes] AS [t1]
INNER JOIN [promo].[CustomerBalanceChanges] AS [t2] ON [t1].[CustomerActionId] = [t2].[CustomerActionId]) ON [t2].[CustomerActionId] = [t0].[Id]


Зауважте, у цьому SQL зник SELECT 1 as [test] для перевірки наявності сутності. І це призводить до того, що такий запит не працює, а завершується з InvalidOperationException: «Значення NULL не може бути присвоєно члену, який є типом System.Int32, що не допускає значення NULL.». Так як linq більше не відстежує свій test-прапор, він намагається чесно скласти сутності CustomerBalanceChange і CustomerPrize з колонок, значення яких NULL, але він не зможе записати NULL наприклад в CustomerBalanceChange.Id, про що і повідомляє нам текст exception'а.
Які методи обходу цієї проблеми існують? Ну, по-перше, можна перефразувати запит так, як він був написаний в першому випадку. Але це зовсім не універсальне рішення, адже хто сказав, що так можна зробити завжди. Linq при першому ж складному запиті може впасти так само, і витрачати час на перестановку join'ів зовсім не хочеться. Та й другий запит семантично відрізняється від першого.
По-друге, ми могли б робити запит до сутностей, а до певним dto, наприклад ось так:

CustomerActions
.SelectMany(ca => 
CustomerPrizes
.Join(CustomerBalanceChanges,
cp => cp.CustomerAction,
cbch => cbch.CustomerAction,
(cp, cbch) => new { cbch, cp })
.Where(cbchcp => cbchcp.cbch.CustomerAction == ca)
.DefaultIfEmpty(),
(ca, cbchcp) => new { cbchcp.cbch, cbchcp.cp, ca})
.Select(cacbchcp => new { 
CustomerActionId = cacbchcp.ca.Id 
CustomerBalanceChangeId = (int?)cacbchcp.cbch.Id 
CustomerPrizeId = (int?)cacbchcp.cp.Id 
} )


Так як CustomerBalanceChangeId та CustomerPrizeId тепер nullable, проблем не виникає. Але нас не може влаштовувати такий підхід, адже нас можуть бути потрібні саме сутності (які ми хочемо змінювати, видаляти або викликати функції на них). Так що є прямолінійний третій спосіб об'єднання, в якому перевірка на null буде проводиться на боці sql:

CustomerActions
.SelectMany(ca => 
CustomerPrizes
.Join(CustomerBalanceChanges,
cp => cp.CustomerAction,
cbch => cbch.CustomerAction,
(cp, cbch) => new { cbch, cp })
.Where(cbchcp => cbchcp.cbch.CustomerAction == ca)
.DefaultIfEmpty(),
(ca, cbchcp) => new { 
cbch = cbchcp == null ? null : cbchcp.cbch, 
cp = cbchcp == null ? null : cbchcp.cp, 
ca
})
.Dump();


Це транслюється не такий вже й страшний sql, як може здатися з першого погляду:

SELECT 
(CASE 
WHEN [t3].[test] IS NULL THEN 1
ELSE 0
END) AS [value], [t3].[Id], [t3].[ChangeAmount], [t3].[Comments], [t3].[CustomerActionId], [t3].[AdminSiteComments], [t3].[BalanceId], [t3].[Id2], [t3].[PrizeId], [t3].[SaleFactId], [t3].[PromoMechanicsName], [t3].[WonCustomerPrizeId], [t3].[PrizeType], [t3].[Published], [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]
FROM [directcrm].[CustomerActions] AS [t0]
LEFT OUTER JOIN (
SELECT 1 AS [test], [t2].[Id], [t2].[ChangeAmount], [t2].[Comments], [t2].[CustomerActionId], [t2].[AdminSiteComments], [t2].[BalanceId], [t1].[Id] AS [Id2], [t1].[PrizeId], [t1].[SaleFactId], [t1].[PromoMechanicsName], [t1].[WonCustomerPrizeId], [t1].[PrizeType], [t1].[Published], [t1].[PromoMechanicsScheduleItemId], [t1].[CustomerActionId] AS [CustomerActionId2]
FROM [promo].[CustomerPrizes] AS [t1]
INNER JOIN [promo].[CustomerBalanceChanges] AS [t2] ON [t1].[CustomerActionId] = [t2].[CustomerActionId]
) AS [t3] ON [t3].[CustomerActionId] = [t0].[Id]


Але, як ви бачите, є нюанс. Запит був не дуже складний, але linq to sql все одно замість того, щоб просто використовувати [t3].[test] кінцевої вибіркою, намалював конструкціюCASE… WHEN. В цьому немає нічого страшного, поки запит не став надто великим. Але якщо таким чином спробувати об'єднати таблиць 10, то підсумкові запити SQL можуть досягати декількох сотень кілобайт! Кілька сотень кілобайт операторів CASE… WHEN.

Крім того, постійно використовувати для простого left outer join'а будь-яку з наведених вище конструкцій кілька накладно, на багато легше було б самому написати extension-метод LeftOuterJoin і використовувати його в запитах. Ось як такий extension виглядає у нас:

public static IQueryable<TResult> LeftOuterJoin<TOuter, TInner, TKey, TResult>(
this 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().Select(
innerValue => Equals(innerValue, default(TInner)) ?
partialResultSelector.Evaluate(outerValue) :
fullResultSelector.Evaluate(outerValue, innerValue));

return outerValues
.GroupJoin(innerValues, outerKeySelector, innerKeySelector, resultSelector.ExpandExpressions())
.SelectMany(result => result);
}


Цей extension транслюється завжди, але в ньому використовується перевірка на значення null на боці sql. Передбачається наступне використання:

var cbchcas = customerActions
.LeftOuterJoin(
context.Repositories
.Get<CustomerBalanceChangeRepository>()
.Items
.Join(context.Repositories
.Get<CustomerPrizeRepository>()
.Items,
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();


SELECT 
(CASE 
WHEN [t3].[test] IS NULL THEN 1
ELSE 0
END) AS [value], [t3].[Id], [t3].[CustomerActionId], [t3].[ChangeAmount], [t3].[Comments], [t3].[AdminSiteComments], [t3].[BalanceId], [t3].[PrizeType], [t3].[Id2], [t3].[PrizeId], [t3].[PromoMechanicsName] AS [PromoMechanicsSystemName], [t3].[Published], [t3].[PromoMechanicsScheduleItemId], [t3].[SaleFactId], [t3].[CustomerActionId2], [t3].[WonCustomerPrizeId], [t0].[Id] AS [Id3], [t0].[DateTimeUtc], [t0].[IsTimeKnown], [t0].[PointOfContactId], [t0].[BrandName] AS [BrandSystemName], [t0].[CreationDateTimeUtc], [t0].[ActionTemplateId], [t0].[CustomerId], [t0].[StaffId], [t0].[OriginalCustomerId], [t0].[IsOriginalCustomerIdExact], [t0].[TransactionalId]
FROM [directcrm].[CustomerActions] AS [t0]
LEFT OUTER JOIN (
SELECT 1 AS [test], [t1].[Id], [t1].[CustomerActionId], [t1].[ChangeAmount], [t1].[Comments], [t1].[AdminSiteComments], [t1].[BalanceId], [t2].[PrizeType], [t2].[Id] AS [Id2], [t2].[PrizeId], [t2].[PromoMechanicsName], [t2].[Published], [t2].[PromoMechanicsScheduleItemId], [t2].[SaleFactId], [t2].[CustomerActionId] AS [CustomerActionId2], [t2].[WonCustomerPrizeId]
FROM [promo].[CustomerBalanceChanges] AS [t1]
INNER JOIN [promo].[CustomerPrizes] AS [t2] ON [t1].[CustomerActionId] = [t2].[CustomerActionId]
) AS [t3] ON [t0].[Id] = [t3].[CustomerActionId]


Ви могли помітити, що в самому extension-методі використовуються методи Evaluate та ExpandExpressions. Це — extension-методи з нашої бібліотеки Mindbox.Expressions. Метод ExpandExpressions рекурсивно обходить всі дерево виразів, на якому він був викликаний, рекурсивно замінюючи виклики Evaluate на вираз, на якому був викликаний Evaluate. Метод ExpandExpressions можна викликати як на об'єктах Expression, так і на IQueryable, що іноді буває зручніше (наприклад, якщо побудова запиту відбувається в кількох місцях). В бібліотеці так само присутня деяка кількість цікавих функцій для рефлексивної роботи з кодом. Можливо, бібліотека виявиться для когось корисною.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.