Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can't select complex property #1287

Open
soshman opened this issue Jul 22, 2024 · 8 comments
Open

Can't select complex property #1287

soshman opened this issue Jul 22, 2024 · 8 comments
Assignees
Labels
bug Something isn't working

Comments

@soshman
Copy link

soshman commented Jul 22, 2024

While experimenting with OData I've encountered a bug with selecting complex properties.

Setup

I have a Transaction entity that contains Id and Price.

{
    public int Id { get; set; }
    public Money Price { get; set; }
}

Money is defined as:

public sealed class Money
{
    public decimal? Amount { get; set; }
    public string? Currency { get; set; }
}

The transaction entity is configured this way using EF Fluent API:

builder.HasKey(p => p.Id);
builder.ComplexProperty(
    p => p.Price,
    propertyBuilder =>
    {
        propertyBuilder.Property(m => m.Amount)
            .HasColumnName("Price");

        propertyBuilder.Property(m => m.Currency)
            .HasColumnName("PriceCurrency");

        propertyBuilder.IsRequired();
    });

My OData configuration is like this:

modelBuilder.ComplexType<Money>();

var transactionEntity = modelBuilder.EntitySet<Transaction>("Transactions").EntityType;
transactionEntity.ComplexProperty(t => t.Price);

I don't use Restier or any other libraries. I set up my controller like this:

public class TransactionsController : ODataController
{
    private readonly DbContext _dataContext;

    public TransactionsController(DbContext dataContext) => _dataContext = dataContext;

    [EnableQuery]
    public ActionResult<IEnumerable<Transaction>> Get() => Ok(_dataContext.Transactions);

    [EnableQuery]
    public async Task<ActionResult<Transaction>> Get([FromRoute] int key)
    {
        if (!await _dataContext.Transactions.AnyAsync(p => p.Id.Equals(key)))
        {
            return NotFound();
        }

        return Ok(SingleResult.Create(_dataContext.Transactions.Where(p => p.Id.Equals(key))));
    }
}

Data model

        <Schema Namespace="Database.Entities" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <EntityType Name="Transaction">
                <Key>
                    <PropertyRef Name="id" />
                </Key>
                <Property Name="price" Type="Database.ValueObjects.Money" Nullable="false" />
                <Property Name="id" Type="Edm.Int32" Nullable="false" />
            </EntityType>
        </Schema>
        <Schema Namespace="Database.ValueObjects" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <ComplexType Name="Money">
                <Property Name="amount" Type="Edm.Decimal" Scale="variable" />
                <Property Name="currency" Type="Edm.String" />
            </ComplexType>
        </Schema>

Scenario

Calling /Transactions and /Transactions(355) works fine.
Calling /Transactions?$select=id,price and /Transactions(355)?$select=id,price throws System.InvalidOperationException with Comparing complex types to null is not supported. message.

Call stack

System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
 ---> System.InvalidOperationException: Comparing complex types to null is not supported.
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.<>c__DisplayClass62_0.<TryRewriteStructuralTypeEquality>g__TryRewriteComplexTypeEquality|1(Expression& result)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.TryRewriteStructuralTypeEquality(ExpressionType nodeType, Expression left, Expression right, Boolean equalsMethod, Expression& result)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
   at Microsoft.EntityFrameworkCore.SqlServer.Query.Internal.SqlServerSqlTranslatingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.TranslateInternal(Expression expression, Boolean applyDefaultTypeMapping)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.TranslateProjection(Expression expression, Boolean applyDefaultTypeMapping)
   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberAssignment(MemberAssignment memberAssignment)
   at System.Linq.Expressions.ExpressionVisitor.VisitMemberBinding(MemberBinding node)
   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberInit(MemberInitExpression memberInitExpression)
   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberAssignment(MemberAssignment memberAssignment)
   at System.Linq.Expressions.ExpressionVisitor.VisitMemberBinding(MemberBinding node)
   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberInit(MemberInitExpression memberInitExpression)
   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberAssignment(MemberAssignment memberAssignment)
   at System.Linq.Expressions.ExpressionVisitor.VisitMemberBinding(MemberBinding node)
   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberInit(MemberInitExpression memberInitExpression)
   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Translate(SelectExpression selectExpression, Expression expression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateSelect(ShapedQueryExpression source, LambdaExpression selector)
   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__DisplayClass9_0`1.<Execute>b__0()
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1.GetEnumerator()
   at System.Collections.Generic.List`1.AddRange(IEnumerable`1 collection)
   at Microsoft.AspNetCore.OData.Query.Container.TruncatedCollection`1..ctor(IQueryable`1 source, Int32 pageSize, Boolean parameterize)
   at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.LimitResults[T](IQueryable`1 queryable, Int32 limit, Boolean parameterize, Boolean& resultsLimited)
   at InvokeStub_ODataQueryOptions.LimitResults(Object, Object, IntPtr*)
   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.InvokeWithFewArgs(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
   at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.LimitResults(IQueryable queryable, Int32 limit, Boolean parameterize, Boolean& resultsLimited)
   at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.ApplyPaging(IQueryable result, ODataQuerySettings querySettings)
   at Microsoft.AspNetCore.OData.Query.ODataQueryOptions.ApplyTo(IQueryable query, ODataQuerySettings querySettings)
   at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
   at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.ExecuteQuery(Object responseValue, IQueryable singleResultCollection, ControllerActionDescriptor actionDescriptor, HttpRequest request)
   at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.OnActionExecuted(ActionExecutedContext actionExecutedContext, Object responseValue, IQueryable singleResultCollection, ControllerActionDescriptor actionDescriptor, HttpRequest request)
   at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.OnActionExecuted(ActionExecutedContext actionExecutedContext)
   at Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute.OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|7_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)

Assemblies

Microsoft.AspNetCore.OData 8.2.5/9.0.0-preview.2/9.0.0-rc.1
Microsoft.EntityFrameworkCore 8.0.6

Additional context

The only relevant thing I was able to find was a bug reported under the GraphQL GitHub page:
ChilliCream/graphql-platform#6604
This may be a similar scenario. EF Core 8 was used in both of these solutions.

Thanks in advance for the help.

@soshman soshman added the bug Something isn't working label Jul 22, 2024
@habbes
Copy link
Contributor

habbes commented Jul 23, 2024

Hello @soshman I am having a hard time reproducing this, could you share the following information:

  • The EF Code database provider you're using
  • The full configuration and setup for your DbContext
  • The full configuration and setup of your EDM model (including the actual instantiation of the modelBuilder so it's clear which class you used to construct it).
  • Some sample data, especially the values of the entity that caused the exception
  • Alternatively, share a link to a repository with the repro project.

@soshman
Copy link
Author

soshman commented Jul 23, 2024

Hi @habbes
Sorry for not being precise enough. We use SQL Server database.
The configuration we use is complicated, so I created a sample project that shows the same problem.

Here's the repo: sample repo
Thanks!

@habbes
Copy link
Contributor

habbes commented Jul 24, 2024

@soshman thanks for sharing the repo. Managed to reproduce the issue. I'll investigate this and try to determine the root cause and get back.

@soshman
Copy link
Author

soshman commented Jul 31, 2024

Hi @habbes! Have you got updates on the matter? Thanks!

@georoni
Copy link

georoni commented Aug 19, 2024

Same error here trying to select Address which is of complex type
GET host/odata/Customers?$select=Id,Name,Address

System.InvalidOperationException: Comparing complex types to null is not supported.

@artizm26
Copy link

@habbes any progress or plan for that?

@soshman
Copy link
Author

soshman commented Sep 9, 2024

@wandeg @habbes It would be great to have some timeframe/plan, so we can proceed with our development or have a decision to change the approach.

@ThomasHeijtink
Copy link

Strange thing here is when using select. The complexproperty is not included in the ef core query. But when using expand it is included but then get the same exception and message. ef core doesn't yet support optional complexproperties. Which is something on their roadmap. But it seems it isn't going to make it in version 9. So it might not be something which needs to be solved here. Although I don't understand why the null check has to be there in the first place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

5 participants