Miroslav Holec
Premium

Hrozba jménem Client Evaluation v Entity Framework Core

Miroslav Holec   2. května 2018  update 20. června 2020

Článek se vztahuje k verzi produktu EF Core 1.0.0

Tento článek byl napsán v roce 2018. Vývojářské technologie se neustále inovují a článek již nemusí popisovat aktuální stav technologie, ideální řešení a můj současný pohled na dané téma.

Jedna ze zajímavých funkcí, které přináší EF Core je tzv. Client vs. Server evaluation. Krom toho, že funkce je velmi užitečná a určitě najde své uplatnění, její nepochopení a neuvážené použítí zcela určitě způsobí v mnoha aplikacích výkonnostní potíže. Pokud jim chcete předejít, přečtěte si tento článek, kde riziko objasním.

👨‍🎓 Nové školení EF Core pro rok 2020

Školení nejnovější verze Entity Framework Core přímo u Vás ve firmě. Naučíte se používat EF Core v celém životním cyklu aplikace od vytvoření modelu, změn v podobě migrací až po dotazování a profilování databázových dotazů.

Více o školení Entity Framework Core

Pracujete-li delší dobu s Entity Framework 6.x, zajisté jste se setkali s chybou ve smyslu:

LINQ to Entities does not recognize the method 'xxx' and this method cannot be translated into a store expression.

Chyba upozorňuje na snahu předat SQL Providerovi nesplnitelný úkol v podobě překladu LINQ dotazu na dotaz databázového úložiště. Většina vývojářů při prvním setkání s touto chybou dlouze bádá, ale časem pochopí příčinu a dokáže se této chybě vyhnout. Příkladem může být dotaz:

var products = db.Products
    .OrderBy(x => x.Price)
    .Select(x => new
    {
        x.Title,
        x.SeoLink,
        Price = UpdatePrice(x.Price)
    })
    .Take(2).ToList();

Nejenže s Entity Framework Core už nevyhučíte na výjimce, ale dokonce dostanete správné a očekávané výsledky.

Client vs. Server Evaluation

DB Provider nově dokáže vyhodnocovat část LINQ dotazu na straně databázového serveru a část na straně klienta. Příklad uvedený nahoře by byl přeložen do klasického SELECT dotazu (upraveno pro čitelnost):

SELECT TOP 2 [x].[Title], [x].[SeoLink], [x].[Price]
FROM [Products] AS [x] ORDER BY [x].[Price]

Z SQL dotazu je patrné, že SQL server vrátí všechna potřebná data na klienta a zbytek, tedy provedení metody UpdatePrice() už se děje na klientovi s využitím vlastnosti Price. Vše vypadá jednoduše, ale musíme mít napaměti, že SQL Server vyhodnotí a vrátí data tak, aby s nimi klientská strana mohla dále pracovat. A tady si lze snadno naletět.

V příkladu výše probíhá řazení dle "surové" ceny, což bez problémů dokáže SQL Server. Proto provede řazení, vrátí 2 záznamy a na klientovi se pouze vlastnost Price nastaví na základě výstupu metody UpdatePrice().

Stačí ale drobný přehmat a vše je jinak:

var products = db.Products
    .Select(x => new
    {
        x.Title,
        x.SeoLink,
        Price = UpdatePrice(x.Price)
    })
    .OrderBy(x => x.Price)
    .Take(2).ToList();

Drobná změna v podobě přesunu OrderBy() již generuje o poznání nebezpečnější dotaz, ve kterém už není omezení na první 2 záznamy:

SELECT [x].[Title], [x].[SeoLink], [x].[Price]
FROM [Products] AS [x]

V tomto případě totiž chceme provést řazení až na základě toho, co vzejde z metody UpdatePrice(). Databázový server logicky nemůže takové řazení provést a proto SQL Provider zažádá zcela o všechna data ze serveru, aby nad nimi mohla být provedena metoda UpdatePrice() a teprve následně Take().

Stejné chování by nastalo, pokud by vývojář například použil Where(), v jehož podmínce by bylo něco, co databázový server nedokáže vyhodnotit.

Na první pohled vypadá všechno logicky (a logické to je), nicméně když neznalý vývojář přijde k existujícímu kódu s úkolem nastavit řazení záznamů a v dobré víře vrazí OrderBy() na konec dotazu (což je u EF 6.x standardní praktika), vytvoří v případě tabulek s větším objemem dat výkonnostní problém.

Zakázání Client Evaluation

Máte-li z potenciální hrozby strach, můžete tuto feature zakázat a dotazy sestavovat starým způsobem. Opět bude na vás, abyste se rozhodli co dáte za úkol SQL Serveru a co si přepočítáte na klientské straně ve své aplikaci. Funkci je možné zakázat v DbContextu v metodě OnConfiguring():

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    .......
    optionsBuilder.ConfigureWarnings(w => w.Throw(RelationalEventId.QueryClientEvaluationWarning));

    base.OnConfiguring(optionsBuilder);
}

Jakmile se pokusí kdokoliv o Client Evaluation, dojde k vyhození výjimky:

Vyhození výjimky po přenastavení warningů

Pokud samozřejmě pracujete v menším týmu vývojářů a máte toto chování neustále na mysli, pak nemá smysl funkci vypínat a můžete ji ke své radosti využívat. V případě aplikací, kde si nelze podobné chyby dovolit nebo ve větších týmech s juniornějšími členy bych doporučil jít raději tradiční cestou.

Aktualizace k verzi 3.x

Ve verzi EF Core 3.0 / 3.1 se funkcionalita Client Evaluation vypnula. Vyhodnocení na straně klienta probíhá obvykle pouze na úrovni poslední projekce. V ostatních případech EF Core automaticky vyhodí výjimku. Hrozba je tedy definitivně zažehnána.