diff --git a/src/EFCore/ChangeTracking/ComplexPropertyEntry.cs b/src/EFCore/ChangeTracking/ComplexPropertyEntry.cs
new file mode 100644
index 00000000000..4ca3ec5c003
--- /dev/null
+++ b/src/EFCore/ChangeTracking/ComplexPropertyEntry.cs
@@ -0,0 +1,152 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
+
+namespace Microsoft.EntityFrameworkCore.ChangeTracking;
+
+///
+/// Provides access to change tracking information and operations for a given property of a complex type.
+///
+///
+///
+/// Instances of this class are returned from methods when using the API and it is
+/// not designed to be directly constructed in your application code.
+///
+///
+/// See Accessing tracked entities in EF Core for more information and
+/// examples.
+///
+///
+public class ComplexPropertyEntry : MemberEntry
+{
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// 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.
+ ///
+ [EntityFrameworkInternal]
+ public ComplexPropertyEntry(InternalEntityEntry internalEntry, IComplexProperty complexProperty)
+ : base(internalEntry, complexProperty)
+ {
+ }
+
+ ///
+ /// Gets or sets a value indicating whether any of the properties of the complex type have been modified
+ /// and should be updated in the database when is called.
+ ///
+ ///
+ ///
+ /// Setting this value causes all of the properties of the complex type to be marked as modified or not as appropriate.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ ///
+ public override bool IsModified
+ {
+ get => Metadata.ComplexType.GetFlattenedProperties().Any(property => InternalEntry.IsModified(property));
+ set
+ {
+ foreach (var property in Metadata.ComplexType.GetFlattenedProperties())
+ {
+ InternalEntry.SetPropertyModified(property, isModified: value);
+ }
+ }
+ }
+
+ ///
+ /// Gets the metadata that describes the facets of this property and how it maps to the database.
+ ///
+ public new virtual IComplexProperty Metadata
+ => (IComplexProperty)base.Metadata;
+
+ ///
+ /// Provides access to change tracking information and operations for a given property of this complex type.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ /// The property to access information and operations for.
+ /// An object that exposes change tracking information and operations for the given property.
+ public virtual PropertyEntry Property(IProperty property)
+ {
+ Check.NotNull(property, nameof(property));
+
+ return new PropertyEntry(InternalEntry, property);
+ }
+
+ ///
+ /// Provides access to change tracking information and operations for a given property of this complex type.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ /// The property to access information and operations for.
+ /// An object that exposes change tracking information and operations for the given property.
+ public virtual PropertyEntry Property(string propertyName)
+ {
+ Check.NotEmpty(propertyName, nameof(propertyName));
+
+ return new PropertyEntry(InternalEntry, Metadata.ComplexType.GetProperty(propertyName));
+ }
+
+ ///
+ /// Provides access to change tracking information and operations for all properties of this complex type.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ public virtual IEnumerable Properties
+ => Metadata.ComplexType.GetProperties().Select(property => new PropertyEntry(InternalEntry, property));
+
+ ///
+ /// Provides access to change tracking information and operations for a given property of a nested complex type on this
+ /// complex type.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ /// The property to access information and operations for.
+ /// An object that exposes change tracking information and operations for the given property.
+ public virtual ComplexPropertyEntry ComplexProperty(IComplexProperty property)
+ {
+ Check.NotNull(property, nameof(property));
+
+ return new ComplexPropertyEntry(InternalEntry, property);
+ }
+
+ ///
+ /// Provides access to change tracking information and operations for a given property of a nested complex type on this
+ /// complex type.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ /// The property to access information and operations for.
+ /// An object that exposes change tracking information and operations for the given property.
+ public virtual ComplexPropertyEntry ComplexProperty(string propertyName)
+ {
+ Check.NotEmpty(propertyName, nameof(propertyName));
+
+ return new ComplexPropertyEntry(InternalEntry, Metadata.ComplexType.GetComplexProperty(propertyName));
+ }
+
+ ///
+ /// Provides access to change tracking information and operations for all properties of nested complex types on this complex type.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ public virtual IEnumerable ComplexProperties
+ => Metadata.ComplexType.GetComplexProperties().Select(property => new ComplexPropertyEntry(InternalEntry, property));
+}
diff --git a/src/EFCore/ChangeTracking/ComplexPropertyEntry`.cs b/src/EFCore/ChangeTracking/ComplexPropertyEntry`.cs
new file mode 100644
index 00000000000..aacc6f56626
--- /dev/null
+++ b/src/EFCore/ChangeTracking/ComplexPropertyEntry`.cs
@@ -0,0 +1,198 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
+
+namespace Microsoft.EntityFrameworkCore.ChangeTracking;
+
+///
+/// Provides access to change tracking information and operations for a given property of a complex type.
+///
+///
+///
+/// Instances of this class are returned from methods when using the API and it is
+/// not designed to be directly constructed in your application code.
+///
+///
+/// See Accessing tracked entities in EF Core for more information and
+/// examples.
+///
+///
+/// The type of the entity type that contains the property.
+/// The type of the property.
+public class ComplexPropertyEntry : ComplexPropertyEntry
+ where TEntity : class
+{
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// 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.
+ ///
+ [EntityFrameworkInternal]
+ public ComplexPropertyEntry(InternalEntityEntry internalEntry, IComplexProperty complexProperty)
+ : base(internalEntry, complexProperty)
+ {
+ }
+
+ ///
+ /// The to which this member belongs.
+ ///
+ /// An entry for the entity that owns this member.
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ public new virtual EntityEntry EntityEntry
+ => new(InternalEntry);
+
+ ///
+ /// Gets or sets the value currently assigned to this property. If the current value is set using this property,
+ /// the change tracker is aware of the change and is not required
+ /// for the context to detect the change.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ public new virtual TComplexProperty CurrentValue
+ {
+ get => InternalEntry.GetCurrentValue(Metadata);
+ set => base.CurrentValue = value;
+ }
+
+ ///
+ /// Provides access to change tracking information and operations for a given property of this complex type.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ ///
+ /// A lambda expression representing the property to access information and operations for.
+ ///
+ /// An object that exposes change tracking information and operations for the given property.
+ public virtual PropertyEntry Property(
+ Expression> propertyExpression)
+ {
+ Check.NotNull(propertyExpression, nameof(propertyExpression));
+
+ return new PropertyEntry(
+ InternalEntry,
+ Metadata.ComplexType.GetProperty(propertyExpression.GetMemberAccess().GetSimpleMemberName()));
+ }
+
+ ///
+ /// Provides access to change tracking information and operations for a given complex type property of this complex type.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ ///
+ /// A lambda expression representing the property to access information and operations for.
+ ///
+ /// An object that exposes change tracking information and operations for the given property.
+ public virtual ComplexPropertyEntry ComplexProperty(
+ Expression> propertyExpression)
+ {
+ Check.NotNull(propertyExpression, nameof(propertyExpression));
+
+ return new ComplexPropertyEntry(
+ InternalEntry,
+ Metadata.ComplexType.GetComplexProperty(propertyExpression.GetMemberAccess().GetSimpleMemberName()));
+ }
+
+ ///
+ /// Provides access to change tracking information and operations for a given property of this complex type.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ /// The type of the property.
+ /// The property to access information and operations for.
+ /// An object that exposes change tracking information and operations for the given property.
+ public virtual PropertyEntry Property(IProperty property)
+ {
+ Check.NotNull(property, nameof(property));
+
+ ValidateType(property);
+
+ return new PropertyEntry(InternalEntry, property);
+ }
+
+ ///
+ /// Provides access to change tracking information and operations for a given complex type property of this complex type.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ /// The type of the property.
+ /// The property to access information and operations for.
+ /// An object that exposes change tracking information and operations for the given property.
+ public virtual ComplexPropertyEntry ComplexProperty(
+ IComplexProperty complexProperty)
+ {
+ Check.NotNull(complexProperty, nameof(complexProperty));
+
+ ValidateType(complexProperty);
+
+ return new ComplexPropertyEntry(InternalEntry, complexProperty);
+ }
+
+ ///
+ /// Provides access to change tracking information and operations for a given property of this complex type.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ /// The type of the property.
+ /// The property to access information and operations for.
+ /// An object that exposes change tracking information and operations for the given property.
+ public virtual PropertyEntry Property(string propertyName)
+ {
+ Check.NotEmpty(propertyName, nameof(propertyName));
+
+ ValidateType(Metadata.ComplexType.FindProperty(propertyName));
+
+ return new PropertyEntry(InternalEntry, Metadata.ComplexType.GetProperty(propertyName));
+ }
+
+ ///
+ /// Provides access to change tracking information and operations for a given complex type property of this complex type.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ /// The type of the property.
+ /// The property to access information and operations for.
+ /// An object that exposes change tracking information and operations for the given property.
+ public virtual ComplexPropertyEntry ComplexProperty(string propertyName)
+ {
+ Check.NotEmpty(propertyName, nameof(propertyName));
+
+ ValidateType(Metadata.ComplexType.FindComplexProperty(propertyName));
+
+ return new ComplexPropertyEntry(
+ InternalEntry, Metadata.ComplexType.GetComplexProperty(propertyName));
+ }
+
+ private static void ValidateType(IPropertyBase? property)
+ {
+ if (property != null
+ && property.ClrType != typeof(TProperty))
+ {
+ throw new ArgumentException(
+ CoreStrings.WrongGenericPropertyType(
+ property.Name,
+ property.DeclaringType.ClrType.ShortDisplayName(),
+ property.ClrType.ShortDisplayName(),
+ typeof(TProperty).ShortDisplayName()));
+ }
+ }
+}
diff --git a/src/EFCore/ChangeTracking/EntityEntry.cs b/src/EFCore/ChangeTracking/EntityEntry.cs
index 5c4aed5fd98..70fd20516f0 100644
--- a/src/EFCore/ChangeTracking/EntityEntry.cs
+++ b/src/EFCore/ChangeTracking/EntityEntry.cs
@@ -145,6 +145,7 @@ public virtual MemberEntry Member(IPropertyBase propertyBase)
return propertyBase switch
{
IProperty property => new PropertyEntry(InternalEntry, property),
+ IComplexProperty complexProperty => new ComplexPropertyEntry(InternalEntry, complexProperty),
INavigationBase navigation => navigation.IsCollection
? new CollectionEntry(InternalEntry, navigation)
: new ReferenceEntry(InternalEntry, (INavigation)navigation),
@@ -170,7 +171,13 @@ public virtual MemberEntry Member(string propertyName)
var property = InternalEntry.EntityType.FindProperty(propertyName);
if (property != null)
{
- return new PropertyEntry(InternalEntry, propertyName);
+ return new PropertyEntry(InternalEntry, property);
+ }
+
+ var complexProperty = InternalEntry.EntityType.FindComplexProperty(propertyName);
+ if (complexProperty != null)
+ {
+ return new ComplexPropertyEntry(InternalEntry, complexProperty);
}
var navigation = (INavigationBase?)InternalEntry.EntityType.FindNavigation(propertyName)
@@ -178,8 +185,8 @@ public virtual MemberEntry Member(string propertyName)
if (navigation != null)
{
return navigation.IsCollection
- ? new CollectionEntry(InternalEntry, propertyName)
- : new ReferenceEntry(InternalEntry, propertyName);
+ ? new CollectionEntry(InternalEntry, navigation)
+ : new ReferenceEntry(InternalEntry, (INavigation)navigation);
}
throw new InvalidOperationException(
@@ -194,7 +201,7 @@ public virtual MemberEntry Member(string propertyName)
/// examples.
///
public virtual IEnumerable Members
- => Properties.Cast().Concat(Navigations);
+ => Properties.Cast().Concat(ComplexProperties).Concat(Navigations);
///
/// Provides access to change tracking information and operations for a given navigation of this entity.
@@ -239,7 +246,8 @@ public virtual NavigationEntry Navigation(string propertyName)
: new ReferenceEntry(InternalEntry, propertyName);
}
- if (InternalEntry.EntityType.FindProperty(propertyName) != null)
+ if (InternalEntry.EntityType.FindProperty(propertyName) != null
+ || InternalEntry.EntityType.FindComplexProperty(propertyName) != null)
{
throw new InvalidOperationException(
CoreStrings.NavigationIsProperty(
@@ -303,7 +311,7 @@ public virtual PropertyEntry Property(string propertyName)
{
Check.NotEmpty(propertyName, nameof(propertyName));
- return new PropertyEntry(InternalEntry, propertyName);
+ return new PropertyEntry(InternalEntry, Metadata.GetProperty(propertyName));
}
///
@@ -317,6 +325,48 @@ public virtual PropertyEntry Property(string propertyName)
public virtual IEnumerable Properties
=> InternalEntry.EntityType.GetProperties().Select(property => new PropertyEntry(InternalEntry, property));
+ ///
+ /// Provides access to change tracking information and operations for a given property of a complex type on this entity.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ /// The property to access information and operations for.
+ /// An object that exposes change tracking information and operations for the given property.
+ public virtual ComplexPropertyEntry ComplexProperty(IComplexProperty property)
+ {
+ Check.NotNull(property, nameof(property));
+
+ return new ComplexPropertyEntry(InternalEntry, property);
+ }
+
+ ///
+ /// Provides access to change tracking information and operations for a given property of a complex type on this entity.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ /// The property to access information and operations for.
+ /// An object that exposes change tracking information and operations for the given property.
+ public virtual ComplexPropertyEntry ComplexProperty(string propertyName)
+ {
+ Check.NotEmpty(propertyName, nameof(propertyName));
+
+ return new ComplexPropertyEntry(InternalEntry, Metadata.GetComplexProperty(propertyName));
+ }
+
+ ///
+ /// Provides access to change tracking information and operations for all properties of complex type on this entity.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ public virtual IEnumerable ComplexProperties
+ => Metadata.GetComplexProperties().Select(property => new ComplexPropertyEntry(InternalEntry, property));
+
///
/// Provides access to change tracking and loading information for a reference (i.e. non-collection)
/// navigation that associates this entity to another entity.
diff --git a/src/EFCore/ChangeTracking/EntityEntry`.cs b/src/EFCore/ChangeTracking/EntityEntry`.cs
index 8901fd76d78..05393062e8c 100644
--- a/src/EFCore/ChangeTracking/EntityEntry`.cs
+++ b/src/EFCore/ChangeTracking/EntityEntry`.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
namespace Microsoft.EntityFrameworkCore.ChangeTracking;
@@ -56,7 +57,30 @@ public virtual PropertyEntry Property(
{
Check.NotNull(propertyExpression, nameof(propertyExpression));
- return new PropertyEntry(InternalEntry, propertyExpression.GetMemberAccess().GetSimpleMemberName());
+ return new PropertyEntry(
+ InternalEntry,
+ Metadata.GetProperty(propertyExpression.GetMemberAccess().GetSimpleMemberName()));
+ }
+
+ ///
+ /// Provides access to change tracking information and operations for a given complex type property of this entity.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ ///
+ /// A lambda expression representing the property to access information and operations for.
+ ///
+ /// An object that exposes change tracking information and operations for the given property.
+ public virtual ComplexPropertyEntry ComplexProperty(
+ Expression> propertyExpression)
+ {
+ Check.NotNull(propertyExpression, nameof(propertyExpression));
+
+ return new ComplexPropertyEntry(
+ InternalEntry,
+ Metadata.GetComplexProperty(propertyExpression.GetMemberAccess().GetSimpleMemberName()));
}
///
@@ -126,6 +150,25 @@ public virtual PropertyEntry Property(IProperty p
return new PropertyEntry(InternalEntry, property);
}
+ ///
+ /// Provides access to change tracking information and operations for a given complex type property of this entity.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ /// The type of the property.
+ /// The property to access information and operations for.
+ /// An object that exposes change tracking information and operations for the given property.
+ public virtual ComplexPropertyEntry ComplexProperty(IComplexProperty complexProperty)
+ {
+ Check.NotNull(complexProperty, nameof(complexProperty));
+
+ ValidateType(complexProperty);
+
+ return new ComplexPropertyEntry(InternalEntry, complexProperty);
+ }
+
///
/// Provides access to change tracking and loading information for a reference (i.e. non-collection)
/// navigation that associates this entity to another entity.
@@ -226,10 +269,29 @@ public virtual PropertyEntry Property(string prop
ValidateType(InternalEntry.EntityType.FindProperty(propertyName));
- return new PropertyEntry(InternalEntry, propertyName);
+ return new PropertyEntry(InternalEntry, Metadata.GetProperty(propertyName));
+ }
+
+ ///
+ /// Provides access to change tracking information and operations for a given complex type property of this entity.
+ ///
+ ///
+ /// See Accessing tracked entities in EF Core for more information and
+ /// examples.
+ ///
+ /// The type of the property.
+ /// The property to access information and operations for.
+ /// An object that exposes change tracking information and operations for the given property.
+ public virtual ComplexPropertyEntry ComplexProperty(string propertyName)
+ {
+ Check.NotEmpty(propertyName, nameof(propertyName));
+
+ ValidateType(InternalEntry.EntityType.FindComplexProperty(propertyName));
+
+ return new ComplexPropertyEntry(InternalEntry, Metadata.GetComplexProperty(propertyName));
}
- private static void ValidateType(IProperty? property)
+ private static void ValidateType(IPropertyBase? property)
{
if (property != null
&& property.ClrType != typeof(TProperty))
diff --git a/src/EFCore/ChangeTracking/NavigationEntry.cs b/src/EFCore/ChangeTracking/NavigationEntry.cs
index b411e0c5da7..bf4bee10548 100644
--- a/src/EFCore/ChangeTracking/NavigationEntry.cs
+++ b/src/EFCore/ChangeTracking/NavigationEntry.cs
@@ -69,7 +69,8 @@ private static INavigationBase GetNavigation(InternalEntityEntry internalEntry,
if (navigation == null)
{
- if (internalEntry.EntityType.FindProperty(name) != null)
+ if (internalEntry.EntityType.FindProperty(name) != null
+ || internalEntry.EntityType.FindComplexProperty(name) != null)
{
throw new InvalidOperationException(
CoreStrings.NavigationIsProperty(
diff --git a/src/EFCore/ChangeTracking/PropertyEntry.cs b/src/EFCore/ChangeTracking/PropertyEntry.cs
index df0d4971046..c62c229c7e6 100644
--- a/src/EFCore/ChangeTracking/PropertyEntry.cs
+++ b/src/EFCore/ChangeTracking/PropertyEntry.cs
@@ -20,18 +20,6 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking;
///
public class PropertyEntry : MemberEntry
{
- ///
- /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
- /// the same compatibility standards as public APIs. It may be changed or removed without notice in
- /// 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.
- ///
- [EntityFrameworkInternal]
- public PropertyEntry(InternalEntityEntry internalEntry, string name)
- : this(internalEntry, internalEntry.EntityType.GetProperty(name))
- {
- }
-
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore/ChangeTracking/PropertyEntry`.cs b/src/EFCore/ChangeTracking/PropertyEntry`.cs
index 750521f9b29..054c97e494d 100644
--- a/src/EFCore/ChangeTracking/PropertyEntry`.cs
+++ b/src/EFCore/ChangeTracking/PropertyEntry`.cs
@@ -23,18 +23,6 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking;
public class PropertyEntry : PropertyEntry
where TEntity : class
{
- ///
- /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
- /// the same compatibility standards as public APIs. It may be changed or removed without notice in
- /// 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.
- ///
- [EntityFrameworkInternal]
- public PropertyEntry(InternalEntityEntry internalEntry, string name)
- : base(internalEntry, name)
- {
- }
-
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore/Metadata/Internal/PropertyBase.cs b/src/EFCore/Metadata/Internal/PropertyBase.cs
index 7efd21b75d4..90f25bb2ac4 100644
--- a/src/EFCore/Metadata/Internal/PropertyBase.cs
+++ b/src/EFCore/Metadata/Internal/PropertyBase.cs
@@ -455,20 +455,30 @@ public static Expression CreateMemberAccess(
complexType.ComplexProperty.GetMemberInfo(forMaterialization: false, forSet: false),
fromStructuralType);
- if (!instanceExpression.Type.IsValueType
- || instanceExpression.Type.IsNullableValueType())
- {
- var instanceVariable = Expression.Variable(instanceExpression.Type, "instance");
- var block = Expression.Block(
- new[] { instanceVariable },
- Expression.Assign(instanceVariable, instanceExpression),
- Expression.Condition(
- Expression.Equal(instanceVariable, Expression.Constant(null)),
- Expression.Default(memberInfo.GetMemberType()),
- Expression.MakeMemberAccess(instanceVariable, memberInfo)));
-
- return block;
- }
+ // TODO: Handle null/default complex types #31376
+ // if (!instanceExpression.Type.IsValueType)
+ // {
+ // var instanceVariable = Expression.Variable(instanceExpression.Type, "instance");
+ // return Expression.Block(
+ // new[] { instanceVariable },
+ // Expression.Assign(instanceVariable, instanceExpression),
+ // Expression.Condition(
+ // Expression.Equal(instanceVariable, Expression.Constant(null)),
+ // forWrite
+ // ? Expression.Block(
+ // Expression.Throw(Expression.Constant(new InvalidOperationException())),
+ // Expression.Default(memberInfo.GetMemberType()))
+ // : Expression.Default(memberInfo.GetMemberType()),
+ // Expression.MakeMemberAccess(instanceExpression, memberInfo)));
+ // }
+
+ // if (instanceExpression.Type.IsNullableValueType())
+ // {
+ // return Expression.Condition(
+ // Expression.Equal(instanceExpression, Expression.Constant(null)),
+ // Expression.Default(memberInfo.GetMemberType()),
+ // Expression.MakeMemberAccess(instanceExpression, memberInfo));
+ // }
}
return Expression.MakeMemberAccess(instanceExpression, memberInfo);
diff --git a/src/EFCore/Metadata/Internal/TypeBaseExtensions.cs b/src/EFCore/Metadata/Internal/TypeBaseExtensions.cs
index 6f35fb8f09c..d1e061d3de8 100644
--- a/src/EFCore/Metadata/Internal/TypeBaseExtensions.cs
+++ b/src/EFCore/Metadata/Internal/TypeBaseExtensions.cs
@@ -56,4 +56,18 @@ public static IReadOnlyDictionary GetRuntimeProperties(thi
///
public static IReadOnlyDictionary GetRuntimeFields(this IReadOnlyTypeBase type)
=> ((TypeBase)type).GetRuntimeFields();
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// 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.
+ ///
+ public static IComplexProperty GetComplexProperty(this ITypeBase type, string name)
+ {
+ Check.NotEmpty(name, nameof(name));
+
+ return type.FindComplexProperty(name)
+ ?? throw new InvalidOperationException(CoreStrings.ComplexPropertyNotFound(type.DisplayName(), name));
+ }
}
diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs
index 5bc400d52b1..563afdfe7df 100644
--- a/src/EFCore/Properties/CoreStrings.Designer.cs
+++ b/src/EFCore/Properties/CoreStrings.Designer.cs
@@ -512,6 +512,14 @@ public static string ComplexPropertyIndexer(object? type, object? property)
GetString("ComplexPropertyIndexer", nameof(type), nameof(property)),
type, property);
+ ///
+ /// The complex property '{type}.{property}' could not be found. Ensure that the property exists and has been included in the model as a complex property.
+ ///
+ public static string ComplexPropertyNotFound(object? type, object? property)
+ => string.Format(
+ GetString("ComplexPropertyNotFound", nameof(type), nameof(property)),
+ type, property);
+
///
/// Configuring the complex property '{type}.{property}' as optional is not supported, call 'IsRequired()'. See https://github.com/dotnet/efcore/issues/31376 for more information.
///
diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx
index 02b81723a70..b9cf6b9e15f 100644
--- a/src/EFCore/Properties/CoreStrings.resx
+++ b/src/EFCore/Properties/CoreStrings.resx
@@ -300,6 +300,9 @@
Adding the complex property '{type}.{property}' as an indexer property isn't supported. See https://github.com/dotnet/efcore/issues/31244 for more information.
+
+ The complex property '{type}.{property}' could not be found. Ensure that the property exists and has been included in the model as a complex property.
+
Configuring the complex property '{type}.{property}' as optional is not supported, call 'IsRequired()'. See https://github.com/dotnet/efcore/issues/31376 for more information.
diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs
index e1f3ccccd02..ab5552d9fa5 100644
--- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs
+++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs
@@ -7921,6 +7921,9 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
AlternateId = new Guid(),
Dependent = new DependentBase(1),
Owned = new OwnedType(c)
+ {
+ Principal = new PrincipalBase()
+ }
});
//c.SaveChanges();
diff --git a/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs b/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs
index c976b910e0f..e20ca73be5f 100644
--- a/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs
+++ b/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs
@@ -26,12 +26,230 @@ protected ComplexTypesTrackingTestBase(TFixture fixture)
[InlineData(EntityState.Modified, true)]
[InlineData(EntityState.Deleted, false)]
[InlineData(EntityState.Deleted, true)]
- public virtual async Task Can_track_entity_with_complex_objects(EntityState state, bool async)
+ public virtual Task Can_track_entity_with_complex_objects(EntityState state, bool async)
+ => TrackAndSaveTest(state, async, CreatePub());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_mark_complex_type_properties_modified(bool trackFromQuery)
+ => MarkModifiedTest(trackFromQuery, CreatePub());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_read_original_values_for_properties_of_complex_types(bool trackFromQuery)
+ => ReadOriginalValuesTest(trackFromQuery, CreatePub());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_write_original_values_for_properties_of_complex_types(bool trackFromQuery)
+ => WriteOriginalValuesTest(trackFromQuery, CreatePub());
+
+ [ConditionalTheory]
+ [InlineData(EntityState.Added, false)]
+ [InlineData(EntityState.Added, true)]
+ [InlineData(EntityState.Unchanged, false)]
+ [InlineData(EntityState.Unchanged, true)]
+ [InlineData(EntityState.Modified, false)]
+ [InlineData(EntityState.Modified, true)]
+ [InlineData(EntityState.Deleted, false)]
+ [InlineData(EntityState.Deleted, true)]
+ public virtual Task Can_track_entity_with_complex_structs(EntityState state, bool async)
+ => TrackAndSaveTest(state, async, CreatePubWithStructs());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_mark_complex_readonly_struct_properties_modified(bool trackFromQuery)
+ => MarkModifiedTest(trackFromQuery, CreatePubWithStructs());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_read_original_values_for_properties_of_structs(bool trackFromQuery)
+ => ReadOriginalValuesTest(trackFromQuery, CreatePubWithStructs());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_write_original_values_for_properties_of_structs(bool trackFromQuery)
+ => WriteOriginalValuesTest(trackFromQuery, CreatePubWithStructs());
+
+ [ConditionalTheory]
+ [InlineData(EntityState.Added, false)]
+ [InlineData(EntityState.Added, true)]
+ [InlineData(EntityState.Unchanged, false)]
+ [InlineData(EntityState.Unchanged, true)]
+ [InlineData(EntityState.Modified, false)]
+ [InlineData(EntityState.Modified, true)]
+ [InlineData(EntityState.Deleted, false)]
+ [InlineData(EntityState.Deleted, true)]
+ public virtual Task Can_track_entity_with_complex_readonly_structs(EntityState state, bool async)
+ => TrackAndSaveTest(state, async, CreatePubWithReadonlyStructs());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_mark_complex_readonly_readonly_struct_properties_modified(bool trackFromQuery)
+ => MarkModifiedTest(trackFromQuery, CreatePubWithReadonlyStructs());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_write_original_values_for_properties_of_readonly_structs(bool trackFromQuery)
+ => WriteOriginalValuesTest(trackFromQuery, CreatePubWithReadonlyStructs());
+
+ [ConditionalTheory]
+ [InlineData(EntityState.Added, false)]
+ [InlineData(EntityState.Added, true)]
+ [InlineData(EntityState.Unchanged, false)]
+ [InlineData(EntityState.Unchanged, true)]
+ [InlineData(EntityState.Modified, false)]
+ [InlineData(EntityState.Modified, true)]
+ [InlineData(EntityState.Deleted, false)]
+ [InlineData(EntityState.Deleted, true)]
+ public virtual Task Can_track_entity_with_complex_record_objects(EntityState state, bool async)
+ => TrackAndSaveTest(state, async, CreatePubWithRecords());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_mark_complex_record_type_properties_modified(bool trackFromQuery)
+ => MarkModifiedTest(trackFromQuery, CreatePubWithRecords());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_read_original_values_for_properties_of_record_complex_types(bool trackFromQuery)
+ => ReadOriginalValuesTest(trackFromQuery, CreatePubWithRecords());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_write_original_values_for_properties_of_record_complex_types(bool trackFromQuery)
+ => WriteOriginalValuesTest(trackFromQuery, CreatePubWithRecords());
+
+ [ConditionalTheory]
+ [InlineData(EntityState.Added, false)]
+ [InlineData(EntityState.Added, true)]
+ [InlineData(EntityState.Unchanged, false)]
+ [InlineData(EntityState.Unchanged, true)]
+ [InlineData(EntityState.Modified, false)]
+ [InlineData(EntityState.Modified, true)]
+ [InlineData(EntityState.Deleted, false)]
+ [InlineData(EntityState.Deleted, true)]
+ public virtual Task Can_track_entity_with_complex_objects_with_fields(EntityState state, bool async)
+ => TrackAndSaveTest(state, async, CreateFieldPub());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_mark_complex_type_properties_modified_with_fields(bool trackFromQuery)
+ => MarkModifiedTest(trackFromQuery, CreateFieldPub());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_read_original_values_for_properties_of_complex_types_with_fields(bool trackFromQuery)
+ => ReadOriginalValuesTest(trackFromQuery, CreateFieldPub());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_write_original_values_for_properties_of_complex_types_with_fields(bool trackFromQuery)
+ => WriteOriginalValuesTest(trackFromQuery, CreateFieldPub());
+
+ [ConditionalTheory]
+ [InlineData(EntityState.Added, false)]
+ [InlineData(EntityState.Added, true)]
+ [InlineData(EntityState.Unchanged, false)]
+ [InlineData(EntityState.Unchanged, true)]
+ [InlineData(EntityState.Modified, false)]
+ [InlineData(EntityState.Modified, true)]
+ [InlineData(EntityState.Deleted, false)]
+ [InlineData(EntityState.Deleted, true)]
+ public virtual Task Can_track_entity_with_complex_structs_with_fields(EntityState state, bool async)
+ => TrackAndSaveTest(state, async, CreateFieldPubWithStructs());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_mark_complex_readonly_struct_properties_modified_with_fields(bool trackFromQuery)
+ => MarkModifiedTest(trackFromQuery, CreateFieldPubWithStructs());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_read_original_values_for_properties_of_structs_with_fields(bool trackFromQuery)
+ => ReadOriginalValuesTest(trackFromQuery, CreateFieldPubWithStructs());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_write_original_values_for_properties_of_structs_with_fields(bool trackFromQuery)
+ => WriteOriginalValuesTest(trackFromQuery, CreateFieldPubWithStructs());
+
+ [ConditionalTheory(Skip = "Constructor binding")]
+ [InlineData(EntityState.Added, false)]
+ [InlineData(EntityState.Added, true)]
+ [InlineData(EntityState.Unchanged, false)]
+ [InlineData(EntityState.Unchanged, true)]
+ [InlineData(EntityState.Modified, false)]
+ [InlineData(EntityState.Modified, true)]
+ [InlineData(EntityState.Deleted, false)]
+ [InlineData(EntityState.Deleted, true)]
+ public virtual Task Can_track_entity_with_complex_readonly_structs_with_fields(EntityState state, bool async)
+ => TrackAndSaveTest(state, async, CreateFieldPubWithReadonlyStructs());
+
+ [ConditionalTheory(Skip = "Constructor binding")]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_mark_complex_readonly_readonly_struct_properties_modified_with_fields(bool trackFromQuery)
+ => MarkModifiedTest(trackFromQuery, CreateFieldPubWithReadonlyStructs());
+
+ [ConditionalTheory(Skip = "Constructor binding")]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_write_original_values_for_properties_of_readonly_structs_with_fields(bool trackFromQuery)
+ => WriteOriginalValuesTest(trackFromQuery, CreateFieldPubWithReadonlyStructs());
+
+ [ConditionalTheory]
+ [InlineData(EntityState.Added, false)]
+ [InlineData(EntityState.Added, true)]
+ [InlineData(EntityState.Unchanged, false)]
+ [InlineData(EntityState.Unchanged, true)]
+ [InlineData(EntityState.Modified, false)]
+ [InlineData(EntityState.Modified, true)]
+ [InlineData(EntityState.Deleted, false)]
+ [InlineData(EntityState.Deleted, true)]
+ public virtual Task Can_track_entity_with_complex_record_objects_with_fields(EntityState state, bool async)
+ => TrackAndSaveTest(state, async, CreateFieldPubWithRecords());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_mark_complex_record_type_properties_modified_with_fields(bool trackFromQuery)
+ => MarkModifiedTest(trackFromQuery, CreateFieldPubWithRecords());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_read_original_values_for_properties_of_record_complex_types_with_fields(bool trackFromQuery)
+ => ReadOriginalValuesTest(trackFromQuery, CreateFieldPubWithRecords());
+
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Can_write_original_values_for_properties_of_record_complex_types_with_fields(bool trackFromQuery)
+ => WriteOriginalValuesTest(trackFromQuery, CreateFieldPubWithRecords());
+
+ private async Task TrackAndSaveTest(EntityState state, bool async, TEntity pub)
+ where TEntity : class
=> await ExecuteWithStrategyInTransactionAsync(
async context =>
{
- var pub = CreatePub();
-
var entry = state switch
{
EntityState.Unchanged => context.Attach(pub),
@@ -54,13 +272,10 @@ public virtual async Task Can_track_entity_with_complex_objects(EntityState stat
}
});
- [ConditionalTheory]
- [InlineData(false)]
- [InlineData(true)]
- public virtual void Can_mark_complex_type_properties_modified(bool trackFromQuery)
+ private void MarkModifiedTest(bool trackFromQuery, TEntity pub)
+ where TEntity : class
{
using var context = CreateContext();
- var pub = CreatePub();
var entry = trackFromQuery ? TrackFromQuery(context, pub) : context.Attach(pub);
@@ -68,61 +283,74 @@ public virtual void Can_mark_complex_type_properties_modified(bool trackFromQuer
AssertPropertyValues(entry);
AssertPropertiesModified(entry, false);
- MarkModified(entry, "EveningActivity.RunnersUp.Members", true);
+ var membersEntry = entry.ComplexProperty("LunchtimeActivity").ComplexProperty("RunnersUp").Property("Members");
+ membersEntry.IsModified = true;
Assert.Equal(EntityState.Modified, entry.State);
- Assert.True(IsModified(entry, "EveningActivity.RunnersUp.Members"));
+ Assert.True(membersEntry.IsModified);
- MarkModified(entry, "LunchtimeActivity.Day", true);
+ var dayEntry = entry.ComplexProperty("LunchtimeActivity").Property("Day");
+ dayEntry.IsModified = true;
Assert.Equal(EntityState.Modified, entry.State);
- Assert.True(IsModified(entry, "LunchtimeActivity.Day"));
+ Assert.True(dayEntry.IsModified);
- MarkModified(entry, "EveningActivity.CoverCharge", true);
+ var coverChargeEntry = entry.ComplexProperty("EveningActivity").Property("CoverCharge");
+ coverChargeEntry.IsModified = true;
Assert.Equal(EntityState.Modified, entry.State);
- Assert.True(IsModified(entry, "EveningActivity.CoverCharge"));
-
- Assert.False(IsModified(entry, "LunchtimeActivity.Name"));
- Assert.False(IsModified(entry, "LunchtimeActivity.Description"));
- Assert.False(IsModified(entry, "LunchtimeActivity.Notes"));
- Assert.False(IsModified(entry, "LunchtimeActivity.CoverCharge"));
- Assert.False(IsModified(entry, "LunchtimeActivity.IsTeamBased"));
- Assert.False(IsModified(entry, "LunchtimeActivity.Champions.Name"));
- Assert.False(IsModified(entry, "LunchtimeActivity.Champions.Members"));
- Assert.False(IsModified(entry, "LunchtimeActivity.RunnersUp.Name"));
- Assert.False(IsModified(entry, "LunchtimeActivity.RunnersUp.Members"));
- Assert.False(IsModified(entry, "EveningActivity.Name"));
- Assert.False(IsModified(entry, "EveningActivity.Day"));
- Assert.False(IsModified(entry, "EveningActivity.Description"));
- Assert.False(IsModified(entry, "EveningActivity.Notes"));
- Assert.False(IsModified(entry, "EveningActivity.IsTeamBased"));
- Assert.False(IsModified(entry, "EveningActivity.Champions.Name"));
- Assert.False(IsModified(entry, "EveningActivity.Champions.Members"));
- Assert.False(IsModified(entry, "EveningActivity.RunnersUp.Name"));
- Assert.False(IsModified(entry, "FeaturedTeam.Name"));
- Assert.False(IsModified(entry, "FeaturedTeam.Members"));
-
- MarkModified(entry, "EveningActivity.RunnersUp.Members", false);
+ Assert.True(coverChargeEntry.IsModified);
+
+ var lunchtimeEntry = entry.ComplexProperty("LunchtimeActivity");
+ var lunchtimeChampionsEntry = lunchtimeEntry.ComplexProperty("Champions");
+ var lunchtimeRunnersUpEntry = lunchtimeEntry.ComplexProperty("RunnersUp");
+ var eveningEntry = entry.ComplexProperty("EveningActivity");
+ var eveningChampionsEntry = eveningEntry.ComplexProperty("Champions");
+ var eveningRunnersUpEntry = eveningEntry.ComplexProperty("RunnersUp");
+ var teamEntry = entry.ComplexProperty("FeaturedTeam");
+
+ Assert.False(lunchtimeEntry.Property("Name").IsModified);
+ Assert.False(lunchtimeEntry.Property("Description").IsModified);
+ Assert.True(lunchtimeEntry.Property("Day").IsModified);
+ Assert.False(lunchtimeEntry.Property("Notes").IsModified);
+ Assert.False(lunchtimeEntry.Property("CoverCharge").IsModified);
+ Assert.False(lunchtimeEntry.Property("IsTeamBased").IsModified);
+ Assert.False(lunchtimeChampionsEntry.Property("Name").IsModified);
+ Assert.False(lunchtimeChampionsEntry.Property("Members").IsModified);
+ Assert.False(lunchtimeRunnersUpEntry.Property("Name").IsModified);
+ Assert.True(lunchtimeRunnersUpEntry.Property("Members").IsModified);
+
+ Assert.False(eveningEntry.Property("Name").IsModified);
+ Assert.False(eveningEntry.Property("Description").IsModified);
+ Assert.False(eveningEntry.Property("Day").IsModified);
+ Assert.False(eveningEntry.Property("Notes").IsModified);
+ Assert.True(eveningEntry.Property("CoverCharge").IsModified);
+ Assert.False(eveningEntry.Property("IsTeamBased").IsModified);
+ Assert.False(eveningChampionsEntry.Property("Name").IsModified);
+ Assert.False(eveningChampionsEntry.Property("Members").IsModified);
+ Assert.False(eveningRunnersUpEntry.Property("Name").IsModified);
+ Assert.False(eveningRunnersUpEntry.Property("Members").IsModified);
+
+ Assert.False(teamEntry.Property("Name").IsModified);
+ Assert.False(teamEntry.Property("Members").IsModified);
+
+ membersEntry.IsModified = false;
Assert.Equal(EntityState.Modified, entry.State);
- Assert.False(IsModified(entry, "EveningActivity.RunnersUp.Members"));
+ Assert.False(membersEntry.IsModified);
- MarkModified(entry, "LunchtimeActivity.Day", false);
+ dayEntry.IsModified = false;
Assert.Equal(EntityState.Modified, entry.State);
- Assert.False(IsModified(entry, "LunchtimeActivity.Day"));
+ Assert.False(dayEntry.IsModified);
- MarkModified(entry, "EveningActivity.CoverCharge", false);
+ coverChargeEntry.IsModified = false;
Assert.Equal(EntityState.Unchanged, entry.State);
- Assert.False(IsModified(entry, "EveningActivity.CoverCharge"));
+ Assert.False(coverChargeEntry.IsModified);
AssertPropertyValues(entry);
AssertPropertiesModified(entry, false);
}
- [ConditionalTheory]
- [InlineData(false)]
- [InlineData(true)]
- public virtual void Can_read_original_values_for_properties_of_complex_types(bool trackFromQuery)
+ private void ReadOriginalValuesTest(bool trackFromQuery, TEntity pub)
+ where TEntity : class
{
using var context = CreateContext();
- var pub = CreatePub();
var entry = trackFromQuery ? TrackFromQuery(context, pub) : context.Attach(pub);
@@ -130,30 +358,64 @@ public virtual void Can_read_original_values_for_properties_of_complex_types(boo
AssertPropertyValues(entry);
AssertPropertiesModified(entry, false);
- WriteCurrentValue(entry, "EveningActivity.Champions.Members", new List { "1", "2", "3" });
+ var membersEntry = entry.ComplexProperty("LunchtimeActivity").ComplexProperty("Champions").Property("Members");
+ membersEntry.CurrentValue = new List { "1", "2", "3" };
+ Assert.Equal(EntityState.Modified, entry.State);
+ Assert.True(membersEntry.IsModified);
+ Assert.Equal(new[] { "1", "2", "3" }, membersEntry.CurrentValue);
+ Assert.Equal(new[] { "Boris", "David", "Theresa" }, membersEntry.OriginalValue);
+
+ var dayEntry = entry.ComplexProperty("LunchtimeActivity").Property("Day");
+ dayEntry.CurrentValue = DayOfWeek.Wednesday;
+ Assert.Equal(EntityState.Modified, entry.State);
+ Assert.True(dayEntry.IsModified);
+ Assert.Equal(DayOfWeek.Wednesday, dayEntry.CurrentValue);
+ Assert.Equal(DayOfWeek.Monday, dayEntry.OriginalValue);
+
+ var coverChargeEntry = entry.ComplexProperty("EveningActivity").Property("CoverCharge");
+ coverChargeEntry.CurrentValue = 3.0m;
+ Assert.Equal(EntityState.Modified, entry.State);
+ Assert.True(coverChargeEntry.IsModified);
+ Assert.Equal(3.0m, coverChargeEntry.CurrentValue);
+ Assert.Equal(5.0m, coverChargeEntry.OriginalValue);
+ }
+
+ private void WriteOriginalValuesTest(bool trackFromQuery, TEntity pub)
+ where TEntity : class
+ {
+ using var context = CreateContext();
+ var entry = trackFromQuery ? TrackFromQuery(context, pub) : context.Attach(pub);
+
+ Assert.Equal(EntityState.Unchanged, entry.State);
+ AssertPropertyValues(entry);
+ AssertPropertiesModified(entry, false);
+
+ var membersEntry = entry.ComplexProperty("EveningActivity").ComplexProperty("Champions").Property("Members");
+ membersEntry.OriginalValue = new List { "1", "2", "3" };
Assert.Equal(EntityState.Modified, entry.State);
- Assert.True(IsModified(entry, "EveningActivity.Champions.Members"));
- Assert.Equal(new[] { "1", "2", "3" }, ReadCurrentValue>(entry, "EveningActivity.Champions.Members"));
- Assert.Equal(
- new[] { "Robert", "Jimmy", "John", "Jason" }, ReadOriginalValue>(entry, "EveningActivity.Champions.Members"));
+ Assert.True(membersEntry.IsModified);
+ Assert.Equal(new[] { "Robert", "Jimmy", "John", "Jason" }, membersEntry.CurrentValue);
+ Assert.Equal(new[] { "1", "2", "3" }, membersEntry.OriginalValue);
- WriteCurrentValue(entry, "LunchtimeActivity.Day", DayOfWeek.Wednesday);
+ var dayEntry = entry.ComplexProperty("LunchtimeActivity").Property("Day");
+ dayEntry.OriginalValue = DayOfWeek.Wednesday;
Assert.Equal(EntityState.Modified, entry.State);
- Assert.True(IsModified(entry, "LunchtimeActivity.Day"));
- Assert.Equal(DayOfWeek.Wednesday, ReadCurrentValue(entry, "LunchtimeActivity.Day"));
- Assert.Equal(DayOfWeek.Monday, ReadOriginalValue(entry, "LunchtimeActivity.Day"));
+ Assert.True(dayEntry.IsModified);
+ Assert.Equal(DayOfWeek.Monday, dayEntry.CurrentValue);
+ Assert.Equal(DayOfWeek.Wednesday, dayEntry.OriginalValue);
- WriteCurrentValue(entry, "EveningActivity.CoverCharge", 3.0m);
+ var coverChargeEntry = entry.ComplexProperty("EveningActivity").Property("CoverCharge");
+ coverChargeEntry.OriginalValue = 3.0m;
Assert.Equal(EntityState.Modified, entry.State);
- Assert.True(IsModified(entry, "EveningActivity.CoverCharge"));
- Assert.Equal(3.0m, ReadCurrentValue(entry, "EveningActivity.CoverCharge"));
- Assert.Equal(5.0m, ReadOriginalValue(entry, "EveningActivity.CoverCharge"));
+ Assert.True(coverChargeEntry.IsModified);
+ Assert.Equal(5.0m, coverChargeEntry.CurrentValue);
+ Assert.Equal(3.0m, coverChargeEntry.OriginalValue);
}
[ConditionalTheory]
[InlineData(false)]
[InlineData(true)]
- public virtual void Can_write_original_values_for_properties_of_complex_types(bool trackFromQuery)
+ public virtual void Detect_changes_in_complex_type_properties(bool trackFromQuery)
{
using var context = CreateContext();
var pub = CreatePub();
@@ -164,32 +426,46 @@ public virtual void Can_write_original_values_for_properties_of_complex_types(bo
AssertPropertyValues(entry);
AssertPropertiesModified(entry, false);
- WriteOriginalValue(entry, "EveningActivity.Champions.Members", new List { "1", "2", "3" });
+ pub.EveningActivity.Champions.Members = new List
+ {
+ "1",
+ "2",
+ "3"
+ };
+ context.ChangeTracker.DetectChanges();
+
+ var membersEntry = entry.ComplexProperty(e => e.EveningActivity).ComplexProperty(e => e.Champions).Property(e => e.Members);
Assert.Equal(EntityState.Modified, entry.State);
- Assert.True(IsModified(entry, "EveningActivity.Champions.Members"));
- Assert.Equal(new[] { "Robert", "Jimmy", "John", "Jason" }, ReadCurrentValue>(entry, "EveningActivity.Champions.Members"));
- Assert.Equal(new[] { "1", "2", "3" }, ReadOriginalValue>(entry, "EveningActivity.Champions.Members"));
+ Assert.True(membersEntry.IsModified);
+ Assert.Equal(new[] { "1", "2", "3" }, membersEntry.CurrentValue);
+ Assert.Equal(new[] { "Robert", "Jimmy", "John", "Jason" }, membersEntry.OriginalValue);
+
+ pub.LunchtimeActivity.Day = DayOfWeek.Wednesday;
+ context.ChangeTracker.DetectChanges();
- WriteOriginalValue(entry, "LunchtimeActivity.Day", DayOfWeek.Wednesday);
+ var dayEntry = entry.ComplexProperty(e => e.LunchtimeActivity).Property(e => e.Day);
Assert.Equal(EntityState.Modified, entry.State);
- Assert.True(IsModified(entry, "LunchtimeActivity.Day"));
- Assert.Equal(DayOfWeek.Monday, ReadCurrentValue(entry, "LunchtimeActivity.Day"));
- Assert.Equal(DayOfWeek.Wednesday, ReadOriginalValue(entry, "LunchtimeActivity.Day"));
+ Assert.True(dayEntry.IsModified);
+ Assert.Equal(DayOfWeek.Wednesday, dayEntry.CurrentValue);
+ Assert.Equal(DayOfWeek.Monday, dayEntry.OriginalValue);
+
+ pub.EveningActivity.CoverCharge = 3.0m;
+ context.ChangeTracker.DetectChanges();
- WriteOriginalValue(entry, "EveningActivity.CoverCharge", 3.0m);
+ var coverChargeEntry = entry.ComplexProperty(e => e.EveningActivity).Property(e => e.CoverCharge);
Assert.Equal(EntityState.Modified, entry.State);
- Assert.True(IsModified(entry, "EveningActivity.CoverCharge"));
- Assert.Equal(5.0m, ReadCurrentValue(entry, "EveningActivity.CoverCharge"));
- Assert.Equal(3.0m, ReadOriginalValue(entry, "EveningActivity.CoverCharge"));
+ Assert.True(coverChargeEntry.IsModified);
+ Assert.Equal(3.0m, coverChargeEntry.CurrentValue);
+ Assert.Equal(5.0m, coverChargeEntry.OriginalValue);
}
[ConditionalTheory]
[InlineData(false)]
[InlineData(true)]
- public virtual void Detect_changes_detects_changes_in_complex_type_properties(bool trackFromQuery)
+ public virtual void Detect_changes_in_complex_struct_type_properties(bool trackFromQuery)
{
using var context = CreateContext();
- var pub = CreatePub();
+ var pub = CreatePubWithStructs();
var entry = trackFromQuery ? TrackFromQuery(context, pub) : context.Attach(pub);
@@ -197,217 +473,303 @@ public virtual void Detect_changes_detects_changes_in_complex_type_properties(bo
AssertPropertyValues(entry);
AssertPropertiesModified(entry, false);
- pub.EveningActivity.Champions.Members = new List
+ var eveningActivity = pub.EveningActivity;
+ var champions = eveningActivity.Champions;
+ champions.Members = new()
{
"1",
"2",
"3"
};
+ eveningActivity.Champions = champions;
+ pub.EveningActivity = eveningActivity;
+
context.ChangeTracker.DetectChanges();
+ var membersEntry = entry.ComplexProperty(e => e.EveningActivity).ComplexProperty(e => e.Champions).Property(e => e.Members);
Assert.Equal(EntityState.Modified, entry.State);
- Assert.True(IsModified(entry, "EveningActivity.Champions.Members"));
- Assert.Equal(new[] { "1", "2", "3" }, ReadCurrentValue>(entry, "EveningActivity.Champions.Members"));
- Assert.Equal(
- new[] { "Robert", "Jimmy", "John", "Jason" }, ReadOriginalValue>(entry, "EveningActivity.Champions.Members"));
+ Assert.True(membersEntry.IsModified);
+ Assert.Equal(new[] { "1", "2", "3" }, membersEntry.CurrentValue);
+ Assert.Equal(new[] { "Robert", "Jimmy", "John", "Jason" }, membersEntry.OriginalValue);
+
+ var lunchtimeActivity = pub.LunchtimeActivity;
+ lunchtimeActivity.Day = DayOfWeek.Wednesday;
+ pub.LunchtimeActivity = lunchtimeActivity;
- pub.LunchtimeActivity.Day = DayOfWeek.Wednesday;
context.ChangeTracker.DetectChanges();
+ var dayEntry = entry.ComplexProperty(e => e.LunchtimeActivity).Property(e => e.Day);
Assert.Equal(EntityState.Modified, entry.State);
- Assert.True(IsModified(entry, "LunchtimeActivity.Day"));
- Assert.Equal(DayOfWeek.Wednesday, ReadCurrentValue(entry, "LunchtimeActivity.Day"));
- Assert.Equal(DayOfWeek.Monday, ReadOriginalValue(entry, "LunchtimeActivity.Day"));
+ Assert.True(dayEntry.IsModified);
+ Assert.Equal(DayOfWeek.Wednesday, dayEntry.CurrentValue);
+ Assert.Equal(DayOfWeek.Monday, dayEntry.OriginalValue);
+
+ eveningActivity = pub.EveningActivity;
+ eveningActivity.CoverCharge = 3.0m;
+ pub.EveningActivity = eveningActivity;
- pub.EveningActivity.CoverCharge = 3.0m;
context.ChangeTracker.DetectChanges();
+ var coverChargeEntry = entry.ComplexProperty(e => e.EveningActivity).Property(e => e.CoverCharge);
Assert.Equal(EntityState.Modified, entry.State);
- Assert.True(IsModified(entry, "EveningActivity.CoverCharge"));
- Assert.Equal(3.0m, ReadCurrentValue(entry, "EveningActivity.CoverCharge"));
- Assert.Equal(5.0m, ReadOriginalValue(entry, "EveningActivity.CoverCharge"));
+ Assert.True(coverChargeEntry.IsModified);
+ Assert.Equal(3.0m, coverChargeEntry.CurrentValue);
+ Assert.Equal(5.0m, coverChargeEntry.OriginalValue);
}
- protected static Pub CreatePub()
- => new()
+ [ConditionalTheory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public virtual void Detects_changes_in_complex_readonly_struct_type_properties(bool trackFromQuery)
+ {
+ using var context = CreateContext();
+ var pub = CreatePubWithReadonlyStructs();
+
+ var entry = trackFromQuery ? TrackFromQuery(context, pub) : context.Attach(pub);
+
+ Assert.Equal(EntityState.Unchanged, entry.State);
+ AssertPropertyValues(entry);
+ AssertPropertiesModified(entry, false);
+
+ pub.EveningActivity = new()
{
- Id = Guid.NewGuid(),
- Name = "The FBI",
- LunchtimeActivity = new()
+ Name = "Music Quiz",
+ Day = DayOfWeek.Friday,
+ Description = "A music pub quiz.",
+ Notes = Array.Empty(),
+ CoverCharge = 5.0m,
+ IsTeamBased = true,
+ Champions = new()
{
- Name = "Pub Quiz",
- Day = DayOfWeek.Monday,
- Description = "A general knowledge pub quiz.",
- Notes = new[] { "One", "Two", "Three" },
- CoverCharge = 2.0m,
- IsTeamBased = true,
- Champions = new()
- {
- Name = "Clueless",
- Members =
- {
- "Boris",
- "David",
- "Theresa"
- }
- },
- RunnersUp = new()
+ Name = "Dazed and Confused",
+ Members = new()
{
- Name = "ZZ",
- Members =
- {
- "Has Beard",
- "Has Beard",
- "Is Called Beard"
- }
- },
+ "1",
+ "2",
+ "3"
+ }
},
- EveningActivity = new()
- {
- Name = "Music Quiz",
- Day = DayOfWeek.Friday,
- Description = "A music pub quiz.",
- Notes = Array.Empty(),
- CoverCharge = 5.0m,
- IsTeamBased = true,
- Champions = new()
+ RunnersUp = new() { Name = "Banksy", Members = new() }
+ };
+
+ context.ChangeTracker.DetectChanges();
+
+ var membersEntry = entry.ComplexProperty(e => e.EveningActivity).ComplexProperty(e => e.Champions).Property(e => e.Members);
+ Assert.Equal(EntityState.Modified, entry.State);
+ Assert.True(membersEntry.IsModified);
+ Assert.Equal(new[] { "1", "2", "3" }, membersEntry.CurrentValue);
+ Assert.Equal(new[] { "Robert", "Jimmy", "John", "Jason" }, membersEntry.OriginalValue);
+
+ pub.LunchtimeActivity = new()
+ {
+ Name = "Pub Quiz",
+ Day = DayOfWeek.Wednesday,
+ Description = "A general knowledge pub quiz.",
+ Notes = new[] { "One", "Two", "Three" },
+ CoverCharge = 2.0m,
+ IsTeamBased = true,
+ Champions = new()
+ {
+ Name = "Clueless",
+ Members = new()
{
- Name = "Dazed and Confused",
- Members =
- {
- "Robert",
- "Jimmy",
- "John",
- "Jason"
- }
- },
- RunnersUp = new()
+ "Boris",
+ "David",
+ "Theresa"
+ }
+ },
+ RunnersUp = new()
+ {
+ Name = "ZZ",
+ Members = new()
{
- Name = "Banksy"
- },
+ "Has Beard",
+ "Has Beard",
+ "Is Called Beard"
+ }
},
- FeaturedTeam = new()
+ };
+
+ context.ChangeTracker.DetectChanges();
+
+ var dayEntry = entry.ComplexProperty(e => e.LunchtimeActivity).Property(e => e.Day);
+ Assert.Equal(EntityState.Modified, entry.State);
+ Assert.True(dayEntry.IsModified);
+ Assert.Equal(DayOfWeek.Wednesday, dayEntry.CurrentValue);
+ Assert.Equal(DayOfWeek.Monday, dayEntry.OriginalValue);
+
+ pub.EveningActivity = new()
+ {
+ Name = "Music Quiz",
+ Day = DayOfWeek.Friday,
+ Description = "A music pub quiz.",
+ Notes = Array.Empty(),
+ CoverCharge = 3.0m,
+ IsTeamBased = true,
+ Champions = new()
{
- Name = "Not In This Lifetime",
- Members =
+ Name = "Dazed and Confused",
+ Members = new()
{
- "Slash",
- "Axl"
+ "1",
+ "2",
+ "3"
}
- }
+ },
+ RunnersUp = new() { Name = "Banksy", Members = new() }
};
- protected void AssertPropertyValues(EntityEntry entry)
- {
- Assert.Equal("The FBI", ReadCurrentValue(entry, "Name"));
- Assert.NotNull(ReadCurrentValue