linq2db.EntityFrameworkCore icon indicating copy to clipboard operation
linq2db.EntityFrameworkCore copied to clipboard

LinqToDb fails with "Association key not found for type"

Open uladz-zubrycki opened this issue 3 years ago • 7 comments

Trying to use linq2db with existing EF code base and found a case, which might be interesting to you as it seems to be valid for EF, but will fail in runtime when linq2db is used as expression engine.

Let's say we have next domain model:

public class Container
{
	public int Id { get; set; }
	public virtual ICollection<Item> Items { get; set; }
	public virtual ICollection<ChildItem> ChildItems { get; set; }
}

public class Item
{
	public int Id { get; set; }
	public int ContainerId { get; set; }
	public virtual ChildItem Child { get; set; }
	public virtual Container Container { get; set; }
}

public class ChildItem
{
	public int Id { get; set; }
	public virtual Item Parent { get; set; }
}

ChildItem here is basically a descendant of Item, but relationship is implemented with use of composition instead of inheritance (migrating from EF 6 to EF Core 3, which doesn't support Table Per Type for hierarchies, hence such design). And of course in real-world model both Item and ChildItem have more attributes and there're multiple Item "descendants" omitted here for brevity.

Configuration for these entities is as follows:

internal sealed class ContainerConfiguration : IEntityTypeConfiguration<Container>
{
	public void Configure(EntityTypeBuilder<Container> _)
	{
		_.HasKey(x => x.Id);
		_.Property(x => x.Id).UseIdentityColumn();
	}
}

internal sealed class ItemConfiguration : IEntityTypeConfiguration<Item>
{
	public void Configure(EntityTypeBuilder<Item> _)
	{
		_.HasKey(x => x.Id);
		_.Property(x => x.Id).UseIdentityColumn();

		_.HasOne(a => a.Container)
			.WithMany(b => b.Items)
			.IsRequired()
			.HasForeignKey(a => a.ContainerId);
	}
}

internal sealed class ChildItemConfiguration : IEntityTypeConfiguration<ChildItem>
{
	public void Configure(EntityTypeBuilder<ChildItem> _)
	{
		_.HasKey(e => e.Id);
		_.Property(e => e.Id).ValueGeneratedNever();

                // semantically each ChildItem is also an Item, hence one-to-one relationship with Id as FK
		_.HasOne(a => a.Parent)
			.WithOne(b => b.Child)
			.IsRequired()
			.HasForeignKey<ChildItem>(a => a.Id);
	}
}

Then this query, which is pretty valid in terms of EF will fail (EF 6 was able to handle that, but EF Core fails as well due to a known bug). Error message is Association key 'ContainerId' not found for type 'ChildItem.

var children = 
    dbContext.Containers
	.Select(c => new
  	    {
		ChildItems = c.ChildItems
			.Select(ch => new
			{
				ContainerId = ch.Parent.ContainerId,
			}),
		})
	.ToLinqToDB()
	.ToArray();

Things work, if I rewrite my query this way, using Items as an entry point and navigating to child in the expression instead of using ChildItems directly.

var children = 
    dbContext.Containers
	.Select(c => new
  	    {
		ChildItems = c.Items
		        .Select(i => i.ChildItem)
			.Select(ch => new
			{
				ContainerId = ch.Parent.ContainerId,
			}),
		})
	.ToLinqToDB()
	.ToArray();

Having said that, do I get it correctly that it's not possible to have ChildItems collection inside the Container type with linq2db?

Here is a code, if you decide to check it out to improve sth in the libraries. Repro.zip

uladz-zubrycki avatar Mar 15 '21 16:03 uladz-zubrycki

It is TPH? If yes, I do not think it will work, linq2db do not support such "feature".

sdanyliv avatar Mar 15 '21 18:03 sdanyliv

@sdanyliv No, it isn't.

All the entities are stored in separate tables and have such relationships:

  • Container has one-to-many association with Item;
  • Item has one-to-one association with ChildItem.

It was "Table per Type" with EF 6: ChildItem was inherited from Item, but now it's implemented with composition on the type level instead, so we have "inheritance" just semantically.

uladz-zubrycki avatar Mar 16 '21 07:03 uladz-zubrycki

This issue is not only present in some exotic table setup. I ran into the same with the simplest possible many-to-one relation between two tables, and managed to replicate it in Microsoft's OData BookStore demo project (slightly modified to use SQLExpress instead of in-memory).

Basically, a query like this

    public IQueryable<Book> Get()
    {
        return _db.Books
            .Where(b => b.Press.Category == Category.Book)
            .ToLinqToDB();
    }

will give the following error

AggregateException: One or more errors occurred. (Association key 'PressId' not found for type 'BookStore.Models.Book.)

System.Threading.Tasks.Task<TResult>.GetResultCore(bool waitCompletionNotification)
System.Threading.Tasks.Task<TResult>.get_Result()
LinqToDB.EntityFrameworkCore.Internal.LinqToDBForEFQueryProvider<T>.GetAsyncEnumerator(CancellationToken cancellationToken)
Microsoft.AspNetCore.Mvc.Infrastructure.AsyncEnumerableReader.ReadInternal<T>(object value)
Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsyncEnumerable(ActionContext context, ObjectResult result, object asyncEnumerable, Func<object, Task<ICollection>> reader)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|29_0<TFilter, TFilterAsync>(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext<TFilter, TFilterAsync>(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Seems like a bug to me. Or am I not using ToLinqToDB() correctly?

Here's the project: BookStoreDotNet5.zip

nforss avatar May 06 '21 07:05 nforss

@nforss I might guess, that proper configuration is missing for the Book entity hence the runtime failure. Also I'm not that sure that it's possible to have navigation property without the foreign key property.

So, pls, add PressId property to the Book type to start with and, if that is not enough, configure the relation explicitly in the OnModelCreating method of your DbContext type.

Hope that helps.

uladz-zubrycki avatar May 06 '21 19:05 uladz-zubrycki

Thank you for the quick reply!

Adding the PressId property to the Book class does indeed fix the problem. I was under the impression that linq2db.EntityFrameworkCore would use the existing EF DbContext mappings to figure these relationships out, but I guess that is not the case. It's of course a bit of a hassle to explicitly type out all the foreign key properties that EF doesn't need, but since we're not using that many classes with LinqToDB, maybe we'll manage.

nforss avatar May 07 '21 06:05 nforss

@nforss It indeed uses the EF model to build its own, it's just not that every feature of EF is supported (and vice versa). So I'm guessing that having a property for a FK is required, but I'm not an author of the library and haven't used it heavily, so might well be mistaken. Try to play with EF entity configuration and/or AssociationAttribute, it might help.

uladz-zubrycki avatar May 07 '21 09:05 uladz-zubrycki

Have found the same behaviour in a 1..* with the child expecting a reference to the parent, using an explicit [ParentName]Id. I'm just trying to use the TempTable as part of EF Core. EF Core can handle the relationship implicitly and I don't want to start spreading [ParentName]Id in all my model files. It's really a shame as otherwise it very much fitted my requirements.

morganics avatar Apr 21 '23 09:04 morganics