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

Shadow property for Primary Key - exception 'No backing field could be found' #28154

Closed
skbtomek opened this issue Jun 3, 2022 · 4 comments
Closed
Labels
closed-out-of-scope This is not something that will be fixed/implemented and the issue is closed. customer-reported

Comments

@skbtomek
Copy link

skbtomek commented Jun 3, 2022

What I would like to achieve?

To have separate domain model identifier (GUID) from database primary key (long).

Domain model

namespace Domain.Models;

public class Question
{
    public Question(Guid questionId, string text)
    {
        QuestionId = questionId;
        Text = text;
    }

    // Domain model identifier, presented to the outside world
    public Guid QuestionId { get; }

    public string Text { get; }

   // persistance detail, don't want to pollute domain model with this property
   // private long _id;
}

Db context:

using Domain.Models;

public class DatabaseContext : DbContext
{
    public DbSet<Question> Questions { get; private set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        var question = modelBuilder.Entity<Question>();
        question.Property<long>("_id");
        question.HasKey("_id");
        
        question.HasIndex(q => q.QuestionId).IsUnique();
        question.Property(q => q.Text);
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("[connection-string]");
    }
}

Use case

Test below fails by throwing exception on: context.Questions.Any(q => q != nameQuestion)

[Fact]
public void Shadow_primary_key_should_be_used_during_referential_equality()
{
    // arrange
    var nameQuestionId = new Guid("45396A1C-08B1-4570-99A5-397AB05FA79E"); 
    using (var context = new DatabaseContext())
    {
        context.Database.EnsureDeleted();
        context.Database.EnsureCreated();
    
        var q1 = new Question(nameQuestionId, "What's your name?");
        var q2 = new Question(Guid.NewGuid(), "What's your number?");

        context.Questions.AddRange(q1, q2);
        context.SaveChanges();            
    }
    
    // act & assert
    using (var context = new DatabaseContext())
    {
        var nameQuestion = context.Questions.SingleOrDefault(q => q.QuestionId == nameQuestionId);

        // throws exception
        var hasPersistedOtherQuestions = context.Questions.Any(q => q != nameQuestion);

        Assert.True(hasPersistedOtherQuestions);
    }
}

Exception

No backing field could be found for property 'Question._id' 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 lambda_method63(Closure , QueryContext )
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
   at System.Linq.Queryable.Any[TSource](IQueryable`1 source, Expression`1 predicate)

Potential solutions that I would like to avoid

Solution 1

Adding private property long _id to Question model

  • it pollutes domain by persistence detail
  • problematic when model will be used in different persistence storage in the future where key type will be different than long

Solution 2

Querying by domain QuestionId instead of relying on reference equality:

var hasPersistedOtherQuestions = context.Questions.Any(q => q.QuestionId != nameQuestion.QuestionId);

Unfortunately, it's easy to forget about it in new queries.

Full project source code

efcore-shadow-pk.zip

Provider and version information

EF Core version: 6.0.5
Database provider: (Microsoft.EntityFrameworkCore.SqlServer)
Target framework: (net6.0)

@ajcvickers
Copy link
Member

This looks like a bug generating queries with shadow primary keys. The entity quality evaluates to PK equality, but there is then an attempt to access a CLR property for the PK when it does not exist.

/cc @smitpatel @maumar

Model:

Model: 
  EntityType: Question
    Properties: 
      _id (no field, long) Shadow Required PK AfterSave:Throw ValueGenerated.OnAdd
      QuestionId (Guid) Required Index
      Text (string) Required
    Keys: 
      _id PK
    Indexes: 
      QuestionId  Unique

@ajcvickers
Copy link
Member

Notes from triage:

var hasPersistedOtherQuestions = context.Questions.Any(q => q != nameQuestion);

In general, this query cannot be made to work with shadow primary keys because the CLR object itself does not have access to the shadow key. It could, perhaps, be made to work if nameQuestion is tracked, but this would be a complex special case.

As a workaround, the unique identity of the domain object can be used, since this is, of course, carried with the CLR object:

var hasPersistedOtherQuestions = context.Questions.Any(q => q.QuestionId != nameQuestion.QuestionId);

@ajcvickers ajcvickers added closed-out-of-scope This is not something that will be fixed/implemented and the issue is closed. and removed needs-design labels Jun 6, 2022
@skbtomek
Copy link
Author

skbtomek commented Jun 6, 2022

@ajcvickers: nameQuestion is tracked, because it was fetched via context/dbset.

// act & assert
using (var context = new DatabaseContext())
{
    var nameQuestion = context.Questions.SingleOrDefault(q => q.QuestionId == nameQuestionId);

    // throws exception
    var hasPersistedOtherQuestions = context.Questions.Any(q => q != nameQuestion);

    Assert.True(hasPersistedOtherQuestions);
}

Screenshot from debugger:
image

@ajcvickers
Copy link
Member

@skbtomek Yes, but what I was trying to say is that even though in a case like this where it could be made to work, the complexity and cost is not worth the value, so we're not going to do so. Especially since it would be a special-case solution for tracking queries only. Generally speaking, putting primary keys in shadow state is a pit-of-failure. It's possible, but not something we would ever recommend.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-out-of-scope This is not something that will be fixed/implemented and the issue is closed. customer-reported
Projects
None yet
Development

No branches or pull requests

2 participants