linq2db / linq2db

Linq to database provider.
MIT License
2.97k stars 456 forks source link

CompiledQuery do not call IEntityServiceInterceptor.EntityCreated()? #4365

Open cal-tlabwest opened 8 months ago

cal-tlabwest commented 8 months ago

Describe your issue

IEntityServiceInterceptor.EntityCreated() is not called when using a CompiledQuery. It works fine using a normal query.

Steps to reproduce

using var db = new MyContext(); // ctor calls DataConnection.ctor(new DataOptions().UseInterceptor(this))

var query = CompiledQuery.Compile<MyContext, CancellationToken, Task<List>>(static (db, ct) => db.MyTable.Where(...).ToListAsync(ct)); var result = await query(db, default); // This will not call MyContext.EntityCreated();

However, this will call EntityCreated(): var query = await db.MyTable.Where(...).ToListAsync(); // This works

Environment details

Linq To DB version: 5.3.2

Database (with version): SQLite 3.42.0

ADO.NET Provider (with version): System.Data.SQLite 1.0.118.0 Operating system: Win11 x64

.NET Version: 8.0

cal-tlabwest commented 6 months ago

I've found the root cause of this problem after some debugging with the linq2db source code. My context, MyContext in the example, implements IDataContext but it doesn't inherit from DataConnection - instead it implements IDataContext by wrapping an internal DataConnection context (for API surface control reasons - we don't want to expose DataConnection in our API as that should be an implementation detail).

This works fine when not using compiled queries; LinqToDB.Linq.Builder.TableBuilder.TableContext.NotifyEntityCreated() checks Builder.DataContext that points to MyContext.Context (the wrapped DataConnection).

However, when using a compiled query LinqToDB.Linq.Builder.TableBuilder.TableContext.NotifyEntityCreated() gets confused as Builder.DataContext now points directly to MyContext.

A workaround is to implement IInterceptable<IEntityServiceInterceptor> in MyContext and just call Context.Interceptor but it would be nice if custom IDataContext classes would work out of the box.

igor-tkachev commented 5 months ago

The following example works just fine:

[Test]
public async Task CompiledQueryTest()
{
    await using var db  = new TestDataConnection();
    using       var map = new IdentityMap(db);

    var query = CompiledQuery.Compile<TestDataConnection,CancellationToken,Task<List<Person>>>(static (db, ct) => db.Person.Where(p => p.ID == 1).ToListAsync(ct));

    var result1 = await query(db, default);
    var result2 = await query(db, default);

    Assert.AreSame(result1[0], result2[0]);
}
cal-tlabwest commented 5 months ago

@igor-tkachev Do you have the complete test code? I'm guessing we do something different in the TestDataConnection class.

igor-tkachev commented 5 months ago

Here it is:

https://github.com/linq2db/linq2db/blob/master/Tests/Linq/Tools/EntityServices/IdentityMapTests.cs

cal-tlabwest commented 5 months ago

@igor-tkachev See PR #4489 with unit tests for this problem. Compiled async query fails but the normal sync query works.