Open JustArchi opened 8 months ago
I am getting a similar exception after the update to .NET 8. However, I am not sure if it is for the same root cause. My entites use strongly typed keys, e.g., there is a struct EntityId<T>
which has a value converter to long
.
The query is similar to yours, e.g., I try to query by some collection and there is a value conversion involved:
public class Book
{
public EntityId<Book> Id { get; set; }
public string Title { get; set; } = "";
public EntityId<Author>? AuthorId { get; set; }
}
ICollection<EntityId<Author>> authorIds = new List<EntityId<Author>> { new(1) };
var books = await context.Set<Book>()
.Where(o => o.AuthorId != null && authorIds.Contains(o.AuthorId.Value))
.ToListAsync();
In .NET 7 with Npgsql.EntityFrameworkCore.PostgreSQL 7.0.11 and Microsoft.AspNetCore.Identity.EntityFrameworkCore 7.0.16, everything works fine. In .NET 8 with Npgsql.EntityFrameworkCore.PostgreSQL 8.0.0 and Microsoft.AspNetCore.Identity.EntityFrameworkCore 8.0.2, the following exception is thrown:
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
---> System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
---> System.ArgumentException: Expression of type 'MyApp.EntityId`1[MyApp.Author]' cannot be used for parameter of type 'System.Nullable`1[MyApp.EntityId`1[MyApp.Author]]' (Parameter 'arg0')
at System.Dynamic.Utils.ExpressionUtils.ValidateOneArgument(MethodBase method, ExpressionType nodeKind, Expression arguments, ParameterInfo pi, String methodParamName, String argumentParamName, Int32 index)
at System.Linq.Expressions.Expression.Invoke(Expression expression, Expression arg0)
at System.Linq.Expressions.Expression.Invoke(Expression expression, IEnumerable`1 arguments)
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.ValueConversion.NpgsqlArrayConverter`3.ArrayConversionExpression[TInput,TOutput,TConcreteOutput](LambdaExpression elementConversionExpression)
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.ValueConversion.NpgsqlArrayConverter`3..ctor(ValueConverter elementConverter)
at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)
--- End of inner exception stack trace ---
at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)
at System.Reflection.MethodBaseInvoker.InvokeWithOneArg(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at System.Reflection.RuntimeConstructorInfo.Invoke(BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at System.RuntimeType.CreateInstanceImpl(BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture) at System.Activator.CreateInstance(Type type, Object[] args)
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping.NpgsqlArrayTypeMapping`3.CreateParameters(String storeType, RelationalTypeMapping elementMapping)
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping.NpgsqlArrayTypeMapping`3..ctor(String storeType, RelationalTypeMapping elementTypeMapping)
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping.NpgsqlArrayTypeMapping`3..ctor(RelationalTypeMapping elementTypeMapping)
at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)
--- End of inner exception stack trace ---
at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)
at System.Reflection.MethodBaseInvoker.InvokeWithOneArg(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at System.Reflection.RuntimeConstructorInfo.Invoke(BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at System.RuntimeType.CreateInstanceImpl(BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture) at System.Activator.CreateInstance(Type type, Object[] args)
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlTypeMappingSource.FindCollectionMapping(RelationalTypeMappingInfo info, Type modelType, Type providerType, CoreTypeMapping elementMapping)
at Microsoft.EntityFrameworkCore.Storage.RelationalTypeMappingSource.<>c.<FindMappingWithConversion>b__8_0(ValueTuple`4 k, RelationalTypeMappingSource self)
at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd[TArg](TKey key, Func`3 valueFactory, TArg factoryArgument)
at Microsoft.EntityFrameworkCore.Storage.RelationalTypeMappingSource.FindMappingWithConversion(RelationalTypeMappingInfo mappingInfo, Type providerClrType, ValueConverter customConverter)
at Microsoft.EntityFrameworkCore.Storage.RelationalTypeMappingSource.FindMapping(Type type, IModel model, CoreTypeMapping elementMapping)
at Npgsql.EntityFrameworkCore.PostgreSQL.Query.NpgsqlSqlExpressionFactory.ApplyTypeMappingsOnItemAndArray(SqlExpression itemExpression, SqlExpression arrayExpression)
at Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal.NpgsqlQueryableMethodTranslatingExpressionVisitor.TranslateContains(ShapedQueryExpression source, Expression item)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.Translate(Expression expression)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.Translate(Expression expression)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.TranslateSubquery(Expression expression)
at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal.NpgsqlSqlTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
at Npgsql.EntityFrameworkCore.PostgreSQL.Query.Internal.NpgsqlSqlTranslatingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.TranslateInternal(Expression expression, Boolean applyDefaultTypeMapping)
at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.Translate(Expression expression, Boolean applyDefaultTypeMapping)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateExpression(Expression expression, Boolean applyDefaultTypeMapping)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateLambdaExpression(ShapedQueryExpression shapedQueryExpression, LambdaExpression lambdaExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateWhere(ShapedQueryExpression source, LambdaExpression predicate)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.Translate(Expression expression)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.Translate(Expression expression)
at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass12_0`1.<ExecuteAsync>b__0()
at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1.GetAsyncEnumerator(CancellationToken cancellationToken)
at System.Runtime.CompilerServices.ConfiguredCancelableAsyncEnumerable`1.GetAsyncEnumerator()
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
at Program.<Main>$(String[] args) in C:\Code\test_postgres_exception\MyApp\Program.cs:line 23
at Program.<Main>(String[] args)
Here is a small example that reproduces the exception: postgres_exception.zip
Thanks for the reports and the repro - I'll try to find some time in the coming week or two to take a look.
Thank you :)
@roji I think I found some problematic code in the the NpgsqlArrayConverter
that could be related to our problem. See line 142.
I used the code from the first comment, e.g., a local variable Dictionary<Task<bool>, ulong> tasks = new();
that is used in the query with .Where(o => !tasks.Values.Contains(o.Id))
. This caused the NpgsqlArrayConverter
to be called with
Say the input type is an IEnumerable<T>
. Then the code tries to construct a type IList<T>
using the following code:
var iListType = typeof(IList<>).MakeGenericType(typeof(TInput).GetGenericArguments()[0]);
This makes the assumption that the concrete type TInput
uses its first generic argument as type for IEnumerable<>
. This is not always true. For example, for Dictionary<TKey, TValue>.ValueCollection
, the first argument is TKey
, but it uses the second argument TValue
for IEnumerable<TValue>
.
Maybe one needs something like this:
var valueType = typeof(TInput).GetInterfaces()
.First(o => o.IsGenericType && o.GetGenericTypeDefinition() == typeof(IEnumerable<>))
.GetGenericArguments()[0];
var iListType = typeof(IList<>).MakeGenericType(valueType);
After making these changes locally, the code continues instead of throwing above exception. However, it calls the converter a second time:
Then, in line 165-169, the code fails with an exception because it cannot find a suitable Dictionary<TKey, TValue>.ValueCollection
constructor. I have no idea how to continue from here.
I wonder why a converter is built here at all. After all, the code only needs to write the values from the enumerable into the query. It should not need to perform any back- and forth-conversion of the underlying collection.
@roji Another possibly problematic code is line 85-87.
The comment says:
// elementConversionExpression is always over non-nullable value types.
Is that so? In first comment, I used a model with a nullable property, e.g., public EntityId<Author>? AuthorId { get; set; }
. The according value converter looks like this:
public class NullableEntityIdToLongConverter<T> : ValueConverter<EntityId<T>?, long?>
{
public NullableEntityIdToLongConverter()
: base(
o => o.HasValue ? o.Value.Value : null,
o => o.HasValue ? new EntityId<T>(o.Value) : null)
{
}
}
I think nullable value types are allowed in converters, arent they? Maybe the NpgsqlArrayConverter
needs to update the elementConversionExpression
accordingly.
@dagophil thanks for the added detail - I still have too many other things going on, but this definitely will be in my list of things to look into for 8.0.3. Sorry for the delay and thanks for your patience...
@roji No problem, thank you for looking into this :)
@dagophil I took a first look here - I could indeed repro the problem (see below for a minimal distilled code sample).
My first question where would be why you have a NullableEntityIdToLongConverter; simply using EntityIdToLongConverter makes the error go away in 8.0, and should work fine (EF handles nulls around value converters, so that converters don't typically need to worry about these).
Note that I'm not saying there's no bug in 8.0 - this code did use to work in 7.0 - but I'm curious whether there's a specific reason you have NullableEntityIdToLongConverter.
@roji
My first question where would be why you have a NullableEntityIdToLongConverter; simply using EntityIdToLongConverter makes the error go away in 8.0, and should work fine (EF handles nulls around value converters, so that converters don't typically need to worry about these).
Well, I did not know that. I guess I overlooked the relevant note in the value converter documentation, but now I found it. I threw away those unnecessary nullable-converters and now everything works as expected. .NET 8 here I come :)
Thank you for your advice!
Alas, I dont think this helps the original poster with their problem, because there are no custom value converters involved.
Yeah, the original issue still stands and I don't believe there is any user error there, not only because it worked fine before, but also because I don't see anything to change or improve in that code in order to solve the exception thrown :slightly_smiling_face:.
Minimum working repro extracted from real application:
This worked fine in EF Core 7 (.NET 7). After update to 8.0.0, it now throws with below details:
For reference, I checked latest upcoming patch version in addition to 8.0.0:
8.0.1-ci.20240105T091620
.The workaround I found suggests this should be easy to fix, although I'm not an expert :slightly_smiling_face:
/cc @roji