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

Static value objects causes InvalidOperationException when using the value object instead of primitive type in predicates #29405

Closed
johannbrink opened this issue Oct 21, 2022 · 3 comments
Labels
closed-no-further-action The issue is closed and no further action is planned. customer-reported

Comments

@johannbrink
Copy link

johannbrink commented Oct 21, 2022

The following line of code causes an InvalidOperationException:

db.Posts.Where(p => p.Status.Equals(Status.Active))

Status is of type ValueObject that implements IComparable and IComparable

Github repo with simplified example code: https://github.com/johannbrink/EFCoreValueObjectPredicateExample

Steps to reproduce

dotnet ef database update
dotnet run

Console output with stack trace

Inserting a new blog
Querying for a blog
Updating the blog and adding a post
This works: Querying for a post (Using primitive type)
THIS BREAKS! Querying for a post (Using Value Object)
Unhandled exception. System.InvalidOperationException: No backing field could be found for property 'Status.PostId' and the property does not have a getter.
   at Microsoft.EntityFrameworkCore.Metadata.IPropertyBase.GetMemberInfo(Boolean forMaterialization, Boolean forSet)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.ClrPropertyGetterFactory.Create(IPropertyBase property)
   at Microsoft.EntityFrameworkCore.Metadata.RuntimePropertyBase.<>c.<Microsoft.EntityFrameworkCore.Metadata.IPropertyBase.GetGetter>b__35_0(RuntimePropertyBase property)
   at Microsoft.EntityFrameworkCore.Internal.NonCapturingLazyInitializer.EnsureInitialized[TParam,TValue](TValue& target, TParam param, Func`2 valueFactory)
   at Microsoft.EntityFrameworkCore.Metadata.RuntimePropertyBase.Microsoft.EntityFrameworkCore.Metadata.IPropertyBase.GetGetter()
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.CreatePropertyAccessExpression(Expression target, IProperty property)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.<>c__DisplayClass51_0.<TryRewriteEntityEquality>b__0(IProperty p)
   at System.Linq.Enumerable.SelectEnumerableIterator`2.MoveNext()
   at System.Linq.Enumerable.Aggregate[TSource](IEnumerable`1 source, Func`3 func)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.TryRewriteEntityEquality(ExpressionType nodeType, Expression left, Expression right, Boolean equalsMethod, Expression& result)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.TranslateInternal(Expression expression)
   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.Translate(Expression expression)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateExpression(Expression expression)
   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 System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   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..ctor(IEnumerable`1 collection)
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at Program.<Main>$(String[] args) in /Users/johann/source/sandbox/EFCoreValueObjectPredicateExample/Program.cs:line 33

EF Core version: 6.0.10
Database provider: Microsoft.EntityFrameworkCore.SqlServer, Microsoft.EntityFrameworkCore.Sqlite
Target framework: (e.g. .NET 6.0)
Operating system: All
IDE: All

@ajcvickers
Copy link
Member

Note for triage: we should consider a better exception message in this case when the property is a shadow property.

@johannbrink The specific exception being thrown is discussed in #28154. However, the underlying issue is that the Status type is being mapped as an entity type, but the code is expecting it to have value object semantics. It is an entity type because owned types are entity types and have keys, even though those keys may be hidden--see below for the model debug view which shows the mapping.

In this case, I would recommend using a value converter to map the Status types as a property in the model, which will then retain the value object semantics and even allow use of the static instances without issue. For example:

public class PostConfiguration: IEntityTypeConfiguration<Post>
{
    public void Configure(EntityTypeBuilder<Post> builder)
    {
        builder.Property(e => e.Status).HasConversion<StatusConverter>();
    }

    private class StatusConverter : ValueConverter<Status, string>
    {
        public StatusConverter() 
            : base(
                v => v.Name,
                v => v == "Active" ? Status.Active : Status.Inactive,
                new RelationalConverterMappingHints(size: 20, unicode: false))
        {
        }
    }
}

Natural queries using the value object will now translate:

var posts2 = db.Posts
    .Where(p => p.Status.Equals(Status.Active)).ToList();
SELECT "p"."PostId", "p"."BlogId", "p"."Content", "p"."Status", "p"."Title"
      FROM "Posts" AS "p"
      WHERE "p"."Status" = 'Active'

Querying into the value object like this won't work:

var posts1 = db.Posts
    .Where(p => p.Status.Name.Equals(Status.Active.Name)).ToList();

This is because the value object contents is opaque to EF and so EF cannot create a translation. It might be possible to do this if #10434 is implemented.


Original mapping as an entity type:

Model: 
  EntityType: Blog
    Properties: 
      BlogId (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Url (string) Required
    Navigations: 
      Posts (List<Post>) Collection ToDependent Post Inverse: Blog
    Keys: 
      BlogId PK
  EntityType: Post
    Properties: 
      PostId (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      BlogId (int) Required FK Index
      Content (string) Required
      Title (string) Required
    Navigations: 
      Blog (Blog) ToPrincipal Blog Inverse: Posts
      Status (Status) ToDependent Status
    Keys: 
      PostId PK
    Foreign keys: 
      Post {'BlogId'} -> Blog {'BlogId'} ToDependent: Posts ToPrincipal: Blog Cascade
    Indexes: 
      BlogId 
  EntityType: Status Owned
    Properties: 
      PostId (no field, int) Shadow Required PK FK AfterSave:Throw
      Name (string) Required MaxLength(20)
    Keys: 
      PostId PK
    Foreign keys: 
      Status {'PostId'} -> Post {'PostId'} Unique Ownership ToDependent: Status Cascade

New mapping as a property:

Model: 
  EntityType: Blog
    Properties: 
      BlogId (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Url (string) Required
    Navigations: 
      Posts (List<Post>) Collection ToDependent Post Inverse: Blog
    Keys: 
      BlogId PK
  EntityType: Post
    Properties: 
      PostId (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      BlogId (int) Required FK Index
      Content (string) Required
      Status (Status) Required
      Title (string) Required
    Navigations: 
      Blog (Blog) ToPrincipal Blog Inverse: Posts
    Keys: 
      PostId PK
    Foreign keys: 
      Post {'BlogId'} -> Blog {'BlogId'} ToDependent: Posts ToPrincipal: Blog Cascade
    Indexes: 
      BlogId 

@ajcvickers
Copy link
Member

Filed #29442 to track creating a better exception message.

@johannbrink
Copy link
Author

Thanks @ajcvickers, that was very helpful. I will add the value converter to my projects and update my queries to a more natural form.

@roji roji closed this as not planned Won't fix, can't repro, duplicate, stale Oct 31, 2022
@roji roji added the closed-no-further-action The issue is closed and no further action is planned. label Oct 31, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-no-further-action The issue is closed and no further action is planned. customer-reported
Projects
None yet
Development

No branches or pull requests

3 participants