[C#/Linq/EF] Expressie werkt alleen met extra variabele

Pagina: 1
Acties:

Onderwerpen


Acties:
  • 0 Henk 'm!

  • Grijze Vos
  • Registratie: December 2002
  • Laatst online: 28-02 22:17
Iemand enig idee waarom het volgende een "Internal .NET Framework Data Provider error 1025" error geeft:

C#:
1
2
3
4
5
6
7
public static Expression<Func<ShowSeat, Boolean>> Test(int flag)
{
    return s => s.SomeEnum.Value == flag;
}

var q =
    context.someTable.Count(Test((int)SomeEnum.SomeValue));


en het onderstaande equivalent niet:

C#:
1
2
3
var expr = Test((int)SomeEnum.SomeValue);
var q =
    context.someTable.Count(expr);

Op zoek naar een nieuwe collega, .NET webdev, voornamelijk productontwikkeling. DM voor meer info


Acties:
  • 0 Henk 'm!

  • D-Raven
  • Registratie: November 2001
  • Laatst online: 10-09 20:32
Mijn gut feeling zegt dat er een verschil zit m.b.t hoe de expression tree ge-evalueerd word. Maar verklaren waarom dan kan ik niet.

Wat gebeurd er als je dit doet?

C#:
1
var q = context.someTable.Count(s => s.SomeEnum.Value == (int)SomeEnum.SomeValue);

Acties:
  • 0 Henk 'm!

  • Grijze Vos
  • Registratie: December 2002
  • Laatst online: 28-02 22:17
Die werkt ook gewoon zoals je zou verwachten.

Op zoek naar een nieuwe collega, .NET webdev, voornamelijk productontwikkeling. DM voor meer info


Acties:
  • 0 Henk 'm!

  • D-Raven
  • Registratie: November 2001
  • Laatst online: 10-09 20:32
Moest even graven maar ben er uit. Had ooit ergens iets gelezen in een blogpost van Jon Skeet. (kan m zo niet meer vinden).
Maar het kwam er op neer dat de Linq provider van EF kan niet goed overweg kan met Func's. De reden waarom ontgaat me even.

Overigens kun je dit wel deels omzeilen blijkbaar: merk op, 2 jaar oude vraag. maar ik denk dat de workaround nog valide is, mits je dit probleem ook hebt in de gevallen waarbij je niet Count gebruikt.

http://stackoverflow.com/...ternal-net-framework-data

[ Voor 41% gewijzigd door D-Raven op 09-01-2012 15:55 ]


Acties:
  • 0 Henk 'm!

  • Grijze Vos
  • Registratie: December 2002
  • Laatst online: 28-02 22:17
Daarom wrap ik ook de Func in een Expression Tree.

Ik denk dat ik eruit ben. Als ik de "var" verander in een "dynamic" krijg ik netjes de foutmelding "Extension methods cannot be dynamically dispatched". Doordat ik geen tussentijdse variabele gebruik denk ik dat de compiler daarom at runtime een type moet gaan verzinnen en dat werkt dan dus niet. Met de tussentijdse variabele heeft ie wel de type informatie, en kan hij wel zijn werk gewoon doen.

Iets anders kan ik me niet bedenken.

[ Voor 81% gewijzigd door Grijze Vos op 09-01-2012 16:04 ]

Op zoek naar een nieuwe collega, .NET webdev, voornamelijk productontwikkeling. DM voor meer info


Acties:
  • 0 Henk 'm!

  • D-Raven
  • Registratie: November 2001
  • Laatst online: 10-09 20:32
Ik weet niet of dat klopt. Op het moment dat je er een dynamic van maakt wordt het namelijk een compleet ander beest. Dan krijg je ook heel anders gedrag, getuige ook de melding die je krijgt.

Hoe dan ook, het blijft vreemd....

Acties:
  • 0 Henk 'm!

  • pedorus
  • Registratie: Januari 2008
  • Niet online
Ik vermoed iets met niet recompilen vanwege een bijv. een bugje en een versie waarbij het nog om een Func<> ipv een Expression<> ging. ;)

Vitamine D tekorten in Nederland | Dodelijk coronaforum gesloten


Acties:
  • 0 Henk 'm!

  • xzaz
  • Registratie: Augustus 2005
  • Laatst online: 11-09 12:49
Ooit eens gehad in een presentatie, had iets te maken met het pas uitvoeren van de code in runtime en niet in build time. Wat technisch de oorzaak was weet ik niet meer.

Misschien was dit een probleem bij LINQ.

Schiet tussen de palen en je scoort!


Acties:
  • 0 Henk 'm!

  • EfBe
  • Registratie: Januari 2000
  • Niet online
De oorzaak is het feit dat bij de 1e 'Test' gezien wordt als onderdeel van de query. De 2e, die goed gaat, stopt de lambda in een variabele die in de expression tree te zien is als een normale constant en ge-inlined wordt. Dit kan niet bij de 1e versie, want 'Test' wordt daar gezien als een functie die onderdeel is van de query en dus probeert de provider die te vertalen naar een SQL statement. Dat dit resulteert in een internal error en niet in een normale error is Microsoft's schuld, maar ja.... entity framework...

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


Acties:
  • 0 Henk 'm!

  • D-Raven
  • Registratie: November 2001
  • Laatst online: 10-09 20:32
EfBe schreef op maandag 09 januari 2012 @ 17:00:
De oorzaak is het feit dat bij de 1e 'Test' gezien wordt als onderdeel van de query. De 2e, die goed gaat, stopt de lambda in een variabele die in de expression tree te zien is als een normale constant en ge-inlined wordt. Dit kan niet bij de 1e versie, want 'Test' wordt daar gezien als een functie die onderdeel is van de query en dus probeert de provider die te vertalen naar een SQL statement. Dat dit resulteert in een internal error en niet in een normale error is Microsoft's schuld, maar ja.... entity framework...
Dat is precies de verklaring die ik destijds in Jon Skeet's post gelezen had. _/-\o_

Acties:
  • 0 Henk 'm!

  • EfBe
  • Registratie: Januari 2000
  • Niet online
D-Raven schreef op maandag 09 januari 2012 @ 17:04:
[...]
Dat is precies de verklaring die ik destijds in Jon Skeet's post gelezen had. _/-\o_
Is ook een van de meest voorkomende fouten bij linq en 1 van de rederen dat Linq soms best wel sucked. De andere is een loop variabele (foreach variable) in een closure gebruiken.

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


Acties:
  • 0 Henk 'm!

  • D-Raven
  • Registratie: November 2001
  • Laatst online: 10-09 20:32
EfBe schreef op maandag 09 januari 2012 @ 17:08:
[...]

Is ook een van de meest voorkomende fouten bij linq en 1 van de rederen dat Linq soms best wel sucked. De andere is een loop variabele (foreach variable) in een closure gebruiken.
Die loop variabele vind ik nog wel redelijk transparant, nouja, laat ik het zo zeggen. Hij is goed uit te leggen.

De fout waar we het over hebben is een stuk lastiger. IMO.

Acties:
  • 0 Henk 'm!

  • Sebazzz
  • Registratie: September 2006
  • Laatst online: 06:48

Sebazzz

3dp

D-Raven schreef op maandag 09 januari 2012 @ 19:27:
[...]

De fout waar we het over hebben is een stuk lastiger. IMO.
Helemaal niet.

Kijk eens mee. Wat is de daadwerkelijke expression? Is dat:
C#:
1
s => s.SomeEnum.Value == flag

of is de expression
C#:
1
Test((int)SomeEnum.SomeValue)


Het laatste. Het eerste zou kunnen, maar het tweede is gewoon geldig, en daarom wordt dit compile time gebruikt.

[Te koop: 3D printers] [Website] Agile tools: [Return: retrospectives] [Pokertime: planning poker]


Acties:
  • 0 Henk 'm!

  • pedorus
  • Registratie: Januari 2008
  • Niet online
Het probleem is ook dat de code in de TS de fout helemaal niet geeft. Je hebt een inner gedeelte nodig, anders wordt er gewoon meteen geëvalueerd. Zie http://stackoverflow.com/...at-will-be-use-in-a-lambd

Simpel testvoorbeeldje:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
        public static Expression<Func<Entity1, Boolean>> Test(int flag)
        {
            return s => s.Id == flag;
        } 
...
            var a = new Model1Container();
            a.Entity1.AddObject(new Entity1() { Id = 1, Name = "test" });
            a.SaveChanges();
            int noproblem = a.Entity1.Count(Test(1));
            var expr = Test(1);
            int good = a.Entity1.Count(x => x.Id == a.Entity1.Count(expr));
            int bad = a.Entity1.Count(x => x.Id == a.Entity1.Count(Test(1)));

Deze code crashed pas bij bad met error 1025.

Vitamine D tekorten in Nederland | Dodelijk coronaforum gesloten


Acties:
  • 0 Henk 'm!

  • D-Raven
  • Registratie: November 2001
  • Laatst online: 10-09 20:32
Jullie illustreren exact mijn punt.
Laat ik het zo zeggen, closures krijg ik nog wel uitgelegd zonder uitgebreid voorbeeld.

Dit laatste is gewoon een stuk lastiger uit te leggen. Misschien leg ik het dan slecht uit oid, maar ik heb meerdere collega's die hier gewoon geen vinger achter krijgen en uiteindelijk maar onthouden dat als ze zoiets aan het doen zijn, ze dit eerst maar in een aparte variabele moeten stoppen.. en de rest maar vergeten.

Acties:
  • 0 Henk 'm!

  • Grijze Vos
  • Registratie: December 2002
  • Laatst online: 28-02 22:17
pedorus schreef op maandag 09 januari 2012 @ 21:36:
Het probleem is ook dat de code in de TS de fout helemaal niet geeft. Je hebt een inner gedeelte nodig, anders wordt er gewoon meteen geëvalueerd.
Je hebt gelijk, ik heb in de oorspronkelijke code een inner gedeelte. Ik had de code wat gereduceerd om het probleem te illustreren.


Het rare is dat de code met tussenvariabele wel gewoon correct vertaald wordt in een stukje SQL code.

[ Voor 12% gewijzigd door Grijze Vos op 10-01-2012 09:38 ]

Op zoek naar een nieuwe collega, .NET webdev, voornamelijk productontwikkeling. DM voor meer info


Acties:
  • 0 Henk 'm!

  • EfBe
  • Registratie: Januari 2000
  • Niet online
Grijze Vos schreef op dinsdag 10 januari 2012 @ 08:57:
[...]

Je hebt gelijk, ik heb in de oorspronkelijke code een inner gedeelte. Ik had de code wat gereduceerd om het probleem te illustreren.


Het rare is dat de code met tussenvariabele wel gewoon correct vertaald wordt in een stukje SQL code.
Is niet raar, omdat de variabele het resultaat van de method aanroep heeft. Maar wanneer jij een method call in een linq query gebruikt, wordt die functie niet aangeroepen. Zet maar een breakpoint in Test en run het 1e stukje code. Je zult zien dat Test nooit gerund wordt. Wat gebeurt is dat een MethodCallExpression wordt ge-creeerd in de expression tree als argument voor Count. De linq provider gaat die method call dan evalueren en gaat zoeken naar een vertaling ervoor. Dat dit crasht is eigenlijk een bug: het had een fatsoenlijke foutmelding moeten geven, nl. geen mapping bekend voor 'Test'. Je kunt nl. 'Test' ook mappen op een SQL functie, en die vertaling wordt dan gebruikt als vertaling voor de MethodCallExpression.

Een linq provider kan normaliter wel local methods evalueren overigens. Dit noemt men 'funcletizing', omdat de oorspronkelijke eerste visitor die dit deed zo heette (in linq to sql). Deze zoekt naar expression tree sub-bomen die niet afhankelijk zijn van query sources. Deze sub-bomen worden dan in-memory gecompileerd (omdat hun uitkomst niet afhankelijk is van de data afkomstig van 1 of meerdere sequences in de query) en gerund en de uitkomst wordt dan gebruikt in de feitelijke expression tree. Jouw 'Test' zou hier eigenlijk wel voor in aanmerking moeten komen.

Als ik tijd heb zal ik kijken of mijn linq to llblgen provider dit wel goed oplost.

[ Voor 23% gewijzigd door EfBe op 10-01-2012 09:48 ]

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


Acties:
  • 0 Henk 'm!

  • Grijze Vos
  • Registratie: December 2002
  • Laatst online: 28-02 22:17
Dat is dus wat ik zou verwachten dat ie zou doen. Ik ben benieuwd of LLBLGen3 dit wel goed doet.

Op zoek naar een nieuwe collega, .NET webdev, voornamelijk productontwikkeling. DM voor meer info


Acties:
  • 0 Henk 'm!

  • EfBe
  • Registratie: Januari 2000
  • Niet online
Grijze Vos schreef op dinsdag 10 januari 2012 @ 09:52:
Dat is dus wat ik zou verwachten dat ie zou doen. Ik ben benieuwd of LLBLGen3 dit wel goed doet.
Zoals te verwachten was werkt het gewoon ;)
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private enum EmployeeId
{
    Sjaak = 1,
    Piet = 2,
}

public static System.Linq.Expressions.Expression<Func<OrderEntity, bool>> TestMethod(int value)
{
    return o => o.EmployeeId == value;
}


[Test]
public void GetCountOfOrdersWithFunctionTest()
{
    using(DataAccessAdapter adapter = new DataAccessAdapter())
    {
        LinqMetaData metaData = new LinqMetaData(adapter);

        var amount = metaData.Order.Count(TestMethod((int)EmployeeId.Sjaak));
        Assert.AreEqual(109, amount);
    }
}


Geeft:
: Initial expression to process:
value(SD.LLBLGen.Pro.LinqSupportClasses.DataSource2`1[NW26.Adapter.EntityClasses.OrderEntity]).Count(o => (o.EmployeeId = Convert(value(LinqTester.AdapterTests.EntityFetches+<>c__DisplayClass0).value)))

Generated Sql query:
Query: SELECT TOP 1 COUNT(*) AS [LPAV_] FROM [Northwind].[dbo].[Orders] [LPLA_1] WHERE ( ( ( ( [LPLA_1].[EmployeeID] = @p1))))
Parameter: @p1 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 1.

Wat me opviel was overigens dat de expression tree al de TestMethod call result in zich had. De expression tree was nl. exact hetzelfde wanneer ik dit deed:
code:
1
2
3
4
5
6
7
8
9
10
11
12
[Test]
public void GetCountOfOrdersWithFunctionTest()
{
    using(DataAccessAdapter adapter = new DataAccessAdapter())
    {
        LinqMetaData metaData = new LinqMetaData(adapter);

        var expr = TestMethod((int)EmployeeId.Sjaak);
        var amount = metaData.Order.Count(expr);
        Assert.AreEqual(109, amount);
    }
}


Dus het lijkt erop dat er wat anders aan de hand is, of dat de code die je hebt opgegeven anders is dan ik heb gebruikt in mn bovenstaande voorbeeld.

(edit) het kan natuurlijk zijn dat in jouw geval de method niet is ge-inlined door de compiler en bij mij wel. Zal kijken of ik dat kan voorkomen met een attribute.

(edit) met [MethodImpl(MethodImplOptions.NoInlining)], komt nog steeds de method result als lambda in de expression te staan ipv de method call.

[ Voor 7% gewijzigd door EfBe op 10-01-2012 10:48 ]

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


Acties:
  • 0 Henk 'm!

  • R4gnax
  • Registratie: Maart 2009
  • Laatst online: 06-09 17:51
Wat je ook nog kunt doen is de expressie-expansie tools uit LinqKit gebruiken. Deze lopen over je gegenereerde expression tree heen en zetten automatisch aanroepen van externe expressies om in inline expressies. Dat werkt gewoon met Entity Framework of Linq-to-SQL queryables.

[ Voor 10% gewijzigd door R4gnax op 10-01-2012 13:19 ]


Acties:
  • 0 Henk 'm!

  • pedorus
  • Registratie: Januari 2008
  • Niet online
EfBe schreef op dinsdag 10 januari 2012 @ 10:44:
Zoals te verwachten was werkt het gewoon ;)
Jouw voorbeeld werkt ook met Entity Framework, je gebruikt nml geen functie binnen een lambda... (Net als het voorbeeld in de TS, zie mijn vorige post) Beetje flauwe manier van testen dus. ;)

Vitamine D tekorten in Nederland | Dodelijk coronaforum gesloten


Acties:
  • 0 Henk 'm!

  • EfBe
  • Registratie: Januari 2000
  • Niet online
pedorus schreef op dinsdag 10 januari 2012 @ 17:04:
[...]
Jouw voorbeeld werkt ook met Entity Framework, je gebruikt nml geen functie binnen een lambda... (Net als het voorbeeld in de TS, zie mijn vorige post) Beetje flauwe manier van testen dus. ;)
aahhh, maar dat had ik gemist. Niet flauw, ik heb gewoon de TS query gebruikt. Ik zal met jouw voorbeeld opnieuw testen en kijken wat de expression tree is.

(edit)
Expression tree is:
value(SD.LLBLGen.Pro.LinqSupportClasses.DataSource2`1[NW26.Adapter.EntityClasses.OrderEntity]).Count(x => (x.OrderId = value(LinqTester.AdapterTests.EntityFetches+<>c__DisplayClass2).metaData.Order.Count(TestMethod(1))))
wanneer ik doe:

code:
1
2
3
4
5
6
7
8
9
10
11
[Test]
public void GetCountOfOrdersWithFunctionTest()
{
    using(DataAccessAdapter adapter = new DataAccessAdapter())
    {
        LinqMetaData metaData = new LinqMetaData(adapter);

        var amount = metaData.Order.Count(x=>x.OrderId == metaData.Order.Count(TestMethod((int)EmployeeId.Sjaak)));
        Assert.AreEqual(109, amount);
    }
}

en die crasht ook in mijn code, omdat hij een in-memory candidate (de testmethod call) niet verwacht op die plek, want die zou al ge-evalueerd moeten zijn. (bug te fixen voor mij, niet relevant verder voor deze thread).

De expression tree laat wel zien dat de method call naar TestMethod in de boom zit en dus wel degelijk de oorzaak is. Ook weer opgelost! :)

* EfBe gaat nu 'even' een bug fixen

(edit) F1x0red. (build 3.1.01102012)
code:
1
2
3
Generated Sql query: 
    Query: SELECT TOP 1 COUNT(*) AS [LPAV_] FROM [Northwind].[dbo].[Orders]  [LPLA_1]   WHERE ( ( ( ( [LPLA_1].[OrderID] = (SELECT COUNT(*) AS [LPAV_] FROM [Northwind].[dbo].[Orders]  [LPLA_2]   WHERE ( ( [LPLA_2].[EmployeeID] = @p1)))))))
    Parameter: @p1 : Int32. Length: 0. Precision: 10. Scale: 0. Direction: Input. Value: 1.

*pfew*

[ Voor 63% gewijzigd door EfBe op 10-01-2012 19:21 ]

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


Acties:
  • 0 Henk 'm!

  • Grijze Vos
  • Registratie: December 2002
  • Laatst online: 28-02 22:17
R4gnax schreef op dinsdag 10 januari 2012 @ 13:18:
Wat je ook nog kunt doen is de expressie-expansie tools uit LinqKit gebruiken. Deze lopen over je gegenereerde expression tree heen en zetten automatisch aanroepen van externe expressies om in inline expressies. Dat werkt gewoon met Entity Framework of Linq-to-SQL queryables.
Ik heb maar besloten op die paar plekken waar ik het wou toepassen maar gewoon met de hand mijn expressies te schrijven. Waar ik het voor wilde gebruiken is het queryen van een integer die als backing field diende voor een Enum. Aangezien EF4.0 nog geen enum support heeft.

@EfBe, wanneer komt LLBLGen met een code first oplossing? :)

Op zoek naar een nieuwe collega, .NET webdev, voornamelijk productontwikkeling. DM voor meer info


Acties:
  • 0 Henk 'm!

  • EfBe
  • Registratie: Januari 2000
  • Niet online
Grijze Vos schreef op donderdag 12 januari 2012 @ 14:58:
@EfBe, wanneer komt LLBLGen met een code first oplossing? :)
Niet, want code first is niet zinnig, wanneer je een designer hebt die dingen veel sneller voor je produceert dan code dat kan. En als je wilt typen, gebruik je toch quickmodel, statements tikken om je model te creeeren?:

Customer.Orders 1n Order.Customer

en ik heb 2 entities gemaakt en 1 relatie met 2 navigators erop.

Code first lijkt leuk in het begin. Maar ga maar eens een 100+ entity model beheren vanuit 'code first' oogpunt. Het overzicht raak je snel kwijt. Voor de mensen die dat onzin vinden: je class model != entity model. Bij code first is dat wel het geval, en dat levert vroeg of laat toch problemen op, vooral nadat je code in productie is gegaan en wijzigingen niet zomaar kunnen worden doorgevoerd door 'even' de database opnieuw te creeeren. Bv: maak van een 1:n relatie een 1:1 relatie. Je database wijzigt op een onverwachte plek: je krijgt er een UC bij. Dit kun je niet even migreren in productie. Vanuit je 'code' lijkt het dan logisch dat dit 'even' doorgevoerd wordt maar voor je live model in je productie database is dat wel even andere koek: migratie van data, wat je eerst moet testen in een testomgeving alvorens dat uit te rollen op een productiedatabase.

Tuurlijk heeft EF code-first migrations, althans die komt binnenkort. Maar is dat wel wat je wilt? DB wijzigingen in C# code? Na een aantal wijzigingen op je model, zeker bij grotere modellen loopt dat op, wordt het een onoverzichtelijke brei. Het hele idee dat je 'in de code editor' wel even je model gaat bouwen is onzin, en alleen 'logisch' in de wereld van diegenen die liever dagen spenderen aan het met de hand schrijven van classes en mappings terwijl een programma dat veel sneller voor ze kan doen (en ook nog foutloos).

Maar ieder z'n kicks natuurlijk.

[ Voor 10% gewijzigd door EfBe op 12-01-2012 15:12 ]

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


Acties:
  • 0 Henk 'm!

  • Grijze Vos
  • Registratie: December 2002
  • Laatst online: 28-02 22:17
Nee, ik wil eigenlijk alleen de persistence layer van een O/R mapper. Ik heb eigen modellen van waaruit ik code genereer. Ik genereer nu EF Code First vanuit deze modellen.

(Transformeren naar een EF model is vele malen complexer dan POCO's genereren.)

[ Voor 19% gewijzigd door Grijze Vos op 12-01-2012 15:16 ]

Op zoek naar een nieuwe collega, .NET webdev, voornamelijk productontwikkeling. DM voor meer info


Acties:
  • 0 Henk 'm!

  • EfBe
  • Registratie: Januari 2000
  • Niet online
Grijze Vos schreef op donderdag 12 januari 2012 @ 15:15:
Nee, ik wil eigenlijk alleen de persistence layer van een O/R mapper. Ik heb eigen modellen van waaruit ik code genereer. Ik genereer nu EF Code First vanuit deze modellen.

(Transformeren naar een EF model is vele malen complexer dan POCO's genereren.)
Je hebt dus louter entities en geen mappings / relational model data ? Je kunt dan niets anders dan code first gebruiken en je database volledig je model laten volgen. Lijkt me geen goede basis voor een onderhoudbaar systeem.

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com

Pagina: 1