-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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
Conversation
@roji There should be enough here for query. Just call |
Thanks, I'll experiment with this (and review) tomorrow. I think I have pretty much everything aside from change tracking working at this point. |
Am integrating this into my query test work, and it seems to be working well!
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, |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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."
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Correct. |
@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:
|
src/EFCore/Metadata/IEntityType.cs
Outdated
/// Returns all properties, including those on complex types. | ||
/// </summary> | ||
/// <returns>The properties.</returns> | ||
IEnumerable<IProperty> GetFlattenedProperties() |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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...
There is no |
There was a problem hiding this 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; } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔥
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
@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:
This seems to be #31378. |
Also, only amateurs call ctx.Add(new Blog { Name = "foo" });
ctx.Add(new SpecialBlog { Name = "bar", Details = new BlogDetails { SomeDetail = "X", SomeDetail2 = "Y" } }); |
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 ;) |
Thanks, makes sense! That should mean that at least TPT/TPC tests should be possible. |
@roji Merged |
/// Returns all properties that implement <see cref="IProperty"/>, including those on complex types. | ||
/// </summary> | ||
/// <returns>The properties.</returns> | ||
IEnumerable<IProperty> GetFlattenedProperties() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filed #31493
Part of #9906