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

Initial change tracking and DetectChanges for complex types #31453

Merged
merged 5 commits into from
Aug 13, 2023

Conversation

ajcvickers
Copy link
Member

Part of #9906

@ajcvickers ajcvickers requested a review from a team August 12, 2023 12:59
@ajcvickers
Copy link
Member Author

@roji There should be enough here for query. Just call IStateManager.StartTrackingFromQuery with the fully materialized entity instance.

@roji
Copy link
Member

roji commented Aug 12, 2023

Thanks, I'll experiment with this (and review) tomorrow. I think I have pretty much everything aside from change tracking working at this point.

@roji
Copy link
Member

roji commented Aug 13, 2023

Am integrating this into my query test work, and it seems to be working well!

Just call IStateManager.StartTrackingFromQuery with the fully materialized entity instance

Just to verify, since we don't support tracking complex types on their own (without their containing entity type), no actual materializer change should be necessary here (for change tracking), right? I mean, we already call StartTrackingFromQuery for entity types etc.

@@ -44,7 +44,7 @@ protected virtual TAccessor Create(MemberInfo memberInfo, IPropertyBase? propert
{
var boundMethod = propertyBase != null
? GenericCreate.MakeGenericMethod(
propertyBase.DeclaringType.ClrType,
propertyBase.DeclaringType.ContainingEntityType.ClrType,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI this caused some passing tests in my query PR to fail after rebasing on top of this. This seems to mean that the property accessor for a nested property on a complex type requires its top-level containing entity instance rather than its direct container (which is possibly a complex type).

I don't think this will work for query, since there are situations where we have the containing instance (complex type) and we just need to get the property off it. For example:

public virtual Task Contains_over_complex_type(bool async)
{
    var address = new Address
    {
        AddressLine1 = "804 S. Lakeshore Road",
        ZipCode = 38654,
        Country = new Country { FullName = "United States", Code = "US" }
    };

    return AssertQuery(
        async,
        ss => ss.Set<Customer>().Where(
            c => ss.Set<Customer>().Select(c => c.ShippingAddress).Contains(address)));
}

Here a parameterized Address is provided, and we need to get its properties to send as individual parameters to the database; there's no Customer at all...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where in the code does this happen?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here.

In the PR that adds complex type querying, this needs to be able to receive a parameter value representing a (possibly nested) complex type, and extract a property value from it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roji Pushed an update with GetStructuralTypeClrValue that should work as you need it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks - I can see the tests passing with this.

We should consider doing it the other way around though - the new code calls GetStructuralTypeClrValue even on an entity type, since it doesn't necessarily know/care whether the property is on an entity or complex type; it feels like the default GetClrValue should just accept an instance of the property's declaring CLR type.

But not a blocker, we can discuss in design later and adjust.

Copy link
Member Author

@ajcvickers ajcvickers Aug 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except GetClrValue accepts a parameter called entity and it is constrained to be a reference type. (Although I think we violate this in some of our code gen.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except GetClrValue accepts a parameter called entity and it is constrained to be a reference type. (Although I think we violate this in some of our code gen.)

Do you see a specific problem with removing that constraint (or changing the parameter name)? Currently it seems like we're calling GetStructuralTypeClrValue in all kinds of places where the entity vs. complex type distinction doesn't/shouldn't matter...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. That's when it is called. When it can be any "structural type", as opposed to being an entity type. So one method is "Get the property value given its containing entity instance" and the other is, "Get the property value given an instance of the type on which the property is defined."

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK.

This is purely a naming thing, so we can discuss later in API review. But I'd expect the simple GetClrValue to simply behave like GetStructuralTypeClrValue, and have a specially-named GetClrValueFromContainingEntity for those cases where you want it to do the (considerably more complex) thing of traversing into an arbitrarily deep graph from the containing entity all the way to the leaf property. In any case, there's nothing in the GetClrValue name that suggests it requires the containing entity type etc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that would be best if we started from scratch. Just trying to avoid a breaking change here if we don't need it.

@ajcvickers
Copy link
Member Author

Just to verify, since we don't support tracking complex types on their own (without their containing entity type), no actual materializer change should be necessary here (for change tracking), right? I mean, we already call StartTrackingFromQuery for entity types etc.

Correct.

@roji
Copy link
Member

roji commented Aug 13, 2023

@ajcvickers this seems to fail with a complex property on a derived type (any inheritance strategy):

await using var ctx = new BlogContext();
await ctx.Database.EnsureDeletedAsync();
await ctx.Database.EnsureCreatedAsync();

ctx.Blogs.Add(new Blog { Name = "foo" });
ctx.SpecialBlogs.Add(new SpecialBlog { Name = "bar" });
await ctx.SaveChangesAsync();

public class BlogContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<SpecialBlog> SpecialBlogs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer(@"Server=localhost;Database=test;User=SA;Password=Abcd5678;Connect Timeout=60;ConnectRetryCount=0;Encrypt=false")
            //.UseSqlite("Filename=:memory:")
            // .UseNpgsql(@"Host=localhost;Username=test;Password=test")
            .LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<SpecialBlog>().ComplexProperty(b => b.Details);
    }
}

public class Blog
{
    public int Id { get; set; }
    public string? Name { get; set; }
}

public class SpecialBlog : Blog
{
    public BlogDetails Details { get; set; }
}

public class BlogDetails
{
    public string SomeDetail { get; set; }
    public string SomeDetail2 { get; set; }
}

Error:

      System.Diagnostics.UnreachableException: Check.DebugAssert failed: Unexpected null for SpecialBlog.Details#BlogDetails (BlogDetails)
         at Microsoft.EntityFrameworkCore.Utilities.Check.DebugAssert(Boolean condition, String message) in /home/roji/projects/efcore/src/Shared/Check.cs:line 115
         at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.InternalComplexEntry.ReadPropertyValue(IPropertyBase propertyBase) in /home/roji/projects/efcore/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.InternalComplexEntry.cs:line 596
         at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.InternalComplexEntry.get_Item(IPropertyBase propertyBase) in /home/roji/projects/efcore/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.InternalComplexEntry.cs:line 784
         at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.InternalComplexEntry.GetCurrentValue(IPropertyBase propertyBase) in /home/roji/projects/efcore/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.InternalComplexEntry.cs:line 640
         at Microsoft.EntityFrameworkCore.Update.ColumnModification.GetCurrentValue(IUpdateEntry entry, IProperty property) in /home/roji/projects/efcore/src/EFCore.Relational/Update/ColumnModification.cs:line 210
         at Microsoft.EntityFrameworkCore.Update.ColumnModification.get_Value() in /home/roji/projects/efcore/src/EFCore.Relational/Update/ColumnModification.cs:line 156
         at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.AddParameter(IColumnModification columnModification) in /home/roji/projects/efcore/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs:line 303
         at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.AddParameters(IReadOnlyModificationCommand modificationCommand) in /home/roji/projects/efcore/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs:line 281
         at Microsoft.EntityFrameworkCore.SqlServer.Update.Internal.SqlServerModificationCommandBatch.AddCommand(IReadOnlyModificationCommand modificationCommand) in /home/roji/projects/efcore/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs:line 162
         at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.TryAddCommand(IReadOnlyModificationCommand modificationCommand) in /home/roji/projects/efcore/src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs:line 112
         at Microsoft.EntityFrameworkCore.SqlServer.Update.Internal.SqlServerModificationCommandBatch.TryAddCommand(IReadOnlyModificationCommand modificationCommand) in /home/roji/projects/efcore/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommandBatch.cs:line 146
         at Microsoft.EntityFrameworkCore.Update.Internal.CommandBatchPreparer.CreateCommandBatches(IEnumerable`1 commandSet, Boolean moreCommandSets, Boolean assertColumnModification, ParameterNameGenerator parameterNameGenerator)+MoveNext() in /home/roji/projects/efcore/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs:line 125
         at Microsoft.EntityFrameworkCore.Update.Internal.CommandBatchPreparer.BatchCommands(IList`1 entries, IUpdateAdapter updateAdapter)+MoveNext() in /home/roji/projects/efcore/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs:line 78
         at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken) in /home/roji/projects/efcore/src/EFCore.Relational/Update/Internal/BatchExecutor.cs:line 165
         at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken) in /home/roji/projects/efcore/src/EFCore/ChangeTracking/Internal/StateManager.cs:line 1328
         at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken) in /home/roji/projects/efcore/src/EFCore/ChangeTracking/Internal/StateManager.cs:line 1427
         at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken) in /home/roji/projects/efcore/src/EFCore.SqlServer/Storage/Internal/SqlServerExecutionStrategy.cs:line 79
         at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken) in /home/roji/projects/efcore/src/EFCore/DbContext.cs:line 773
Unhandled exception. System.Diagnostics.UnreachableException: Check.DebugAssert failed: Unexpected null for SpecialBlog.Details#BlogDetails (BlogDetails)

/// Returns all properties, including those on complex types.
/// </summary>
/// <returns>The properties.</returns>
IEnumerable<IProperty> GetFlattenedProperties()
Copy link
Member

@roji roji Aug 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move up to ITypeBase

May name it GetAllNestedProperties?

Copy link
Member

@AndriySvyryd AndriySvyryd Aug 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this wouldn't return properties from complex collections

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not.

@@ -44,7 +44,7 @@ protected virtual TAccessor Create(MemberInfo memberInfo, IPropertyBase? propert
{
var boundMethod = propertyBase != null
? GenericCreate.MakeGenericMethod(
propertyBase.DeclaringType.ClrType,
propertyBase.DeclaringType.ContainingEntityType.ClrType,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except GetClrValue accepts a parameter called entity and it is constrained to be a reference type. (Although I think we violate this in some of our code gen.)

Do you see a specific problem with removing that constraint (or changing the parameter name)? Currently it seems like we're calling GetStructuralTypeClrValue in all kinds of places where the entity vs. complex type distinction doesn't/shouldn't matter...

@ajcvickers
Copy link
Member Author

InternalComplexEntry

There is no InternalComplexEntry anymore. If you still have it, then that's likely a bad rebase/merge.

Copy link
Member

@roji roji left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐑 🇮🇹

The two possible concerns are the naming of GetClrValue, and the moving of all the property counts (and related) from structural down to entity. But we can merge and deal with all that later.

@@ -27,7 +27,7 @@ public interface IInternalEntry
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
IRuntimeTypeBase StructuralType { get; }
IRuntimeEntityType EntityType { get; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming this is because even when we update only properties within a complex type, we still update the table represented by its containing entity type right? It seems to make sense, but I'm guessing @AndriySvyryd had something in mind here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Complex objects aren't tracked directly, so they never have an entry in the state manager. Therefore, this can only ever be an entity type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect most of this interface to go away to support collections and value types, so any changes to it meanwhile are fine by me

@@ -181,14 +181,7 @@ void Validate(IConventionTypeBase typeBase)
CoreStrings.ComplexPropertyOptional(typeBase.DisplayName(), complexProperty.Name));
}

if (complexProperty.ComplexType.ClrType.IsValueType)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that we'll support value types in 8.0?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think... it's being considered

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. So far, value types are still in.

@@ -44,7 +44,7 @@ protected virtual TAccessor Create(MemberInfo memberInfo, IPropertyBase? propert
{
var boundMethod = propertyBase != null
? GenericCreate.MakeGenericMethod(
propertyBase.DeclaringType.ClrType,
propertyBase.DeclaringType.ContainingEntityType.ClrType,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK.

This is purely a naming thing, so we can discuss later in API review. But I'd expect the simple GetClrValue to simply behave like GetStructuralTypeClrValue, and have a specially-named GetClrValueFromContainingEntity for those cases where you want it to do the (considerably more complex) thing of traversing into an arbitrarily deep graph from the containing entity all the way to the leaf property. In any case, there's nothing in the GetClrValue name that suggests it requires the containing entity type etc.

/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
int ShadowPropertyCount
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used in ShapedQueryCompilingExpressionVisitor - shouldn't we be able to get e.g. the shadow property count for a complex count? Most of these seem just as relevant for a complex type as they are for entity types, at least from a pure metadata perspective.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the counts are needed, then we can add them back. From a change tracking perspective, these are computed as part of computing the snapshotting indexes, and these only exist for entity types since, as pointed out before, complex types don't have entries.

When passing shadow values to the state manager, you should pass all the shadow values for all contained complex types in a single value buffer along with the shadow properties for the entity type itself. The shadow indexes reflect this.

@ajcvickers
Copy link
Member Author

@roji The code in your test is invalid, although it should throw a better exception. The complex property on the derived type must be non-null, since it is required. So the code needs to be this:

ctx.Blogs.Add(new Blog { Name = "foo" });
ctx.SpecialBlogs.Add(new SpecialBlog { Name = "bar", Details = new BlogDetails { SomeDetail = "X", SomeDetail2 = "Y" } });

However, I then get this exception:

Cannot insert the value NULL into column 'Details_SomeDetail2', table 'AllTogetherNow.dbo.Blogs'; column does not allow nulls. INSERT fails.
   at Microsoft.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
   at Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
   at Microsoft.Data.SqlClient.SqlDataReader.TryHasMoreRows(Boolean& moreRows)
   at Microsoft.Data.SqlClient.SqlDataReader.TryReadInternal(Boolean setTimeout, Boolean& more)
   at Microsoft.Data.SqlClient.SqlDataReader.ReadAsyncExecute(Task task, Object state)
   at Microsoft.Data.SqlClient.SqlDataReader.InvokeAsyncCall[T](SqlDataReaderBaseAsyncCallContext`1 context)

This seems to be #31378.

@ajcvickers
Copy link
Member Author

Also, only amateurs call DbSet.Add. Make it shorter: 😆

ctx.Add(new Blog { Name = "foo" });
ctx.Add(new SpecialBlog { Name = "bar", Details = new BlogDetails { SomeDetail = "X", SomeDetail2 = "Y" } });

@roji
Copy link
Member

roji commented Aug 13, 2023

Ahahahaha touché... I usually do ctx.Blog.Add with target-typed new() but I forgot this time. I'm just wary of any API which accepts object ;)

@roji
Copy link
Member

roji commented Aug 13, 2023

The code in your test is invalid, although it should throw a better exception. The complex property on the derived type must be non-null, since it is required.

Thanks, makes sense! That should mean that at least TPT/TPC tests should be possible.

@ajcvickers ajcvickers merged commit 0611bee into main Aug 13, 2023
7 checks passed
@ajcvickers ajcvickers deleted the 08092023_Bern branch August 13, 2023 17:56
@ajcvickers
Copy link
Member Author

@roji Merged

/// Returns all properties that implement <see cref="IProperty"/>, including those on complex types.
/// </summary>
/// <returns>The properties.</returns>
IEnumerable<IProperty> GetFlattenedProperties()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and GetSnapshottableMembers should be implemented in RuntimeTypeBase in a way that doesn't allocate.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By, "in a way that doesn't allocate", do you mean we should pre-compute this collection?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be precomputed or memoized

@@ -426,7 +426,8 @@ public virtual IComparer<IUpdateEntry> CurrentValueComparer
public static Expression CreateMemberAccess(
IPropertyBase? property,
Expression instanceExpression,
MemberInfo memberInfo)
MemberInfo memberInfo,
bool fromStructuralType)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fromContainingType

fromStructuralType);

if (!instanceExpression.Type.IsValueType
|| instanceExpression.Type.IsNullableValueType())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't there an IsNullable extension method?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't find one.

/// </summary>
/// <param name="complexObject">The complex type instance instance.</param>
/// <returns><see langword="true" /> if the property value is the CLR default; <see langword="false" /> it is any other value.</returns>
bool HasStructuralTypeSentinelValue(object complexObject);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two methods will box value types

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed #31493

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants