diff --git a/src/EFCore.InMemory/Storage/Internal/InMemoryTable.cs b/src/EFCore.InMemory/Storage/Internal/InMemoryTable.cs index e757518d053..fb3b097285f 100644 --- a/src/EFCore.InMemory/Storage/Internal/InMemoryTable.cs +++ b/src/EFCore.InMemory/Storage/Internal/InMemoryTable.cs @@ -368,7 +368,7 @@ private bool HasNullabilityError( return false; } - if (!property.IsNullable && propertyValue == null) + if (!IsNullable(property) && propertyValue == null) { nullabilityErrors.Add(property); @@ -378,6 +378,16 @@ private bool HasNullabilityError( return false; } + private static bool IsNullable(IProperty property) + => property.IsNullable + || (property.DeclaringType is IComplexType complexType + && IsNullable(complexType.ComplexProperty)); + + private static bool IsNullable(IComplexProperty property) + => property.IsNullable + ||( property.DeclaringType is IComplexType complexType + && IsNullable(complexType.ComplexProperty)); + private void ThrowNullabilityErrorException( IUpdateEntry entry, IList nullabilityErrors) diff --git a/src/EFCore.Relational/Update/ColumnModification.cs b/src/EFCore.Relational/Update/ColumnModification.cs index 9961f87ac7b..a77ee091db3 100644 --- a/src/EFCore.Relational/Update/ColumnModification.cs +++ b/src/EFCore.Relational/Update/ColumnModification.cs @@ -205,7 +205,13 @@ private void SetOriginalValue(object? value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public static object? GetCurrentValue(IUpdateEntry entry, IProperty property) - => entry.GetCurrentValue(property); + => property.DeclaringType switch + { + IComplexType { ComplexProperty: var complexProperty } + when complexProperty.IsNullable && !complexProperty.IsCollection && entry.GetCurrentValue(complexProperty) == null + => null, + _ => entry.GetCurrentValue(property) + }; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/Metadata/Builders/ComplexCollectionBuilder`.cs b/src/EFCore/Metadata/Builders/ComplexCollectionBuilder`.cs index a2929ae24f9..80571690313 100644 --- a/src/EFCore/Metadata/Builders/ComplexCollectionBuilder`.cs +++ b/src/EFCore/Metadata/Builders/ComplexCollectionBuilder`.cs @@ -316,6 +316,124 @@ public virtual ComplexCollectionBuilder ComplexProperty( return this; } + /// + /// Returns an object that can be used to configure a complex property of the complex type. + /// If no property with the given name exists, then a new property will be added. + /// + /// + /// When adding a new property, if a property with the same name exists in the complex class + /// then it will be added to the model. If no property exists in the complex class, then + /// a new shadow state complex property will be added. A shadow state property is one that does not have a + /// corresponding property in the complex class. The current value for the property is stored in + /// the rather than being stored in instances of the complex class. + /// + /// The type of the property to be configured. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// An object that can be used to configure the property. + public virtual ComplexPropertyBuilder ComplexProperty( + Expression> propertyExpression) + where TProperty : struct + => new( + TypeBuilder.ComplexProperty( + Check.NotNull(propertyExpression).GetMemberAccess(), + complexTypeName: null, + collection: false, + ConfigurationSource.Explicit)!.Metadata); + + /// + /// Returns an object that can be used to configure a complex property of the complex type. + /// If no property with the given name exists, then a new property will be added. + /// + /// + /// When adding a new property, if a property with the same name exists in the complex class + /// then it will be added to the model. If no property exists in the complex class, then + /// a new shadow state complex property will be added. A shadow state property is one that does not have a + /// corresponding property in the complex class. The current value for the property is stored in + /// the rather than being stored in instances of the complex class. + /// + /// The type of the property to be configured. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// The name of the complex type. + /// An object that can be used to configure the property. + public virtual ComplexPropertyBuilder ComplexProperty( + Expression> propertyExpression, + string complexTypeName) + where TProperty : struct + => new( + TypeBuilder.ComplexProperty( + Check.NotNull(propertyExpression).GetMemberAccess(), + Check.NotEmpty(complexTypeName), + collection: false, + ConfigurationSource.Explicit)!.Metadata); + + /// + /// Configures a complex property of the complex type. + /// If no property with the given name exists, then a new property will be added. + /// + /// + /// When adding a new property, if a property with the same name exists in the complex class + /// then it will be added to the model. If no property exists in the complex class, then + /// a new shadow state complex property will be added. A shadow state property is one that does not have a + /// corresponding property in the complex class. The current value for the property is stored in + /// the rather than being stored in instances of the complex class. + /// + /// The type of the property to be configured. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// An action that performs configuration of the property. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexCollectionBuilder ComplexProperty( + Expression> propertyExpression, + Action> buildAction) + where TProperty : struct + { + Check.NotNull(buildAction); + + buildAction(ComplexProperty(propertyExpression)); + + return this; + } + + /// + /// Configures a complex property of the complex type. + /// If no property with the given name exists, then a new property will be added. + /// + /// + /// When adding a new property, if a property with the same name exists in the complex class + /// then it will be added to the model. If no property exists in the complex class, then + /// a new shadow state complex property will be added. A shadow state property is one that does not have a + /// corresponding property in the complex class. The current value for the property is stored in + /// the rather than being stored in instances of the complex class. + /// + /// The type of the property to be configured. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// The name of the complex type. + /// An action that performs configuration of the property. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexCollectionBuilder ComplexProperty( + Expression> propertyExpression, + string complexTypeName, + Action> buildAction) + where TProperty : struct + { + Check.NotNull(buildAction); + + buildAction(ComplexProperty(propertyExpression, complexTypeName)); + + return this; + } + /// /// Configures a complex collection of the complex type. /// If no property with the given name exists, then a new property will be added. @@ -506,6 +624,96 @@ public virtual ComplexCollectionBuilder ComplexCollection( return this; } + /// + /// Returns an object that can be used to configure a complex collection property of the complex type. + /// If the specified property is not already part of the model, it will be added. + /// + /// The element type. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// An object that can be used to configure the complex collection property. + public virtual ComplexCollectionBuilder ComplexCollection( + Expression?>> propertyExpression) + where TElement : struct + => new( + TypeBuilder.ComplexProperty( + Check.NotNull(propertyExpression).GetMemberAccess(), + complexTypeName: null, + collection: true, + ConfigurationSource.Explicit)!.Metadata); + + /// + /// Returns an object that can be used to configure a complex collection property of the complex type. + /// If the specified property is not already part of the model, it will be added. + /// + /// The element type. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// The name of the complex type. + /// An object that can be used to configure the complex collection property. + public virtual ComplexCollectionBuilder ComplexCollection( + Expression?>> propertyExpression, + string complexTypeName) + where TElement : struct + => new( + TypeBuilder.ComplexProperty( + Check.NotNull(propertyExpression).GetMemberAccess(), + Check.NotEmpty(complexTypeName), + collection: true, + ConfigurationSource.Explicit)!.Metadata); + + /// + /// Configures a complex collection property of the complex type. + /// If the specified property is not already part of the model, it will be added. + /// + /// The element type. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// An action that performs configuration of the property. + /// An object that can be used to configure the complex collection property. + public virtual ComplexCollectionBuilder ComplexCollection( + Expression?>> propertyExpression, + Action> buildAction) + where TElement : struct + { + Check.NotNull(buildAction); + + buildAction(ComplexCollection(propertyExpression)); + + return this; + } + + /// + /// Configures a complex collection property of the complex type. + /// If the specified property is not already part of the model, it will be added. + /// + /// The element type. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// The name of the complex type. + /// An action that performs configuration of the property. + /// An object that can be used to configure the complex collection property. + public virtual ComplexCollectionBuilder ComplexCollection( + Expression?>> propertyExpression, + string complexTypeName, + Action> buildAction) + where TElement : struct + { + Check.NotNull(buildAction); + + buildAction(ComplexCollection(propertyExpression, complexTypeName)); + + return this; + } + /// /// Excludes the given property from the entity type. This method is typically used to remove properties /// or navigations from the entity type that were added by convention. diff --git a/src/EFCore/Metadata/Builders/ComplexPropertyBuilder`.cs b/src/EFCore/Metadata/Builders/ComplexPropertyBuilder`.cs index 9476367275b..8ece80c7f6f 100644 --- a/src/EFCore/Metadata/Builders/ComplexPropertyBuilder`.cs +++ b/src/EFCore/Metadata/Builders/ComplexPropertyBuilder`.cs @@ -278,7 +278,7 @@ public virtual ComplexPropertyBuilder ComplexProperty( ConfigurationSource.Explicit)!.Metadata); /// - /// Configures a complex property of the complex type. + /// Returns an object that can be used to configure a complex property of the complex type. /// If no property with the given name exists, then a new property will be added. /// /// @@ -339,6 +339,124 @@ public virtual ComplexPropertyBuilder ComplexProperty( return this; } + /// + /// Returns an object that can be used to configure a complex property of the complex type. + /// If no property with the given name exists, then a new property will be added. + /// + /// + /// When adding a new property, if a property with the same name exists in the complex class + /// then it will be added to the model. If no property exists in the complex class, then + /// a new shadow state complex property will be added. A shadow state property is one that does not have a + /// corresponding property in the complex class. The current value for the property is stored in + /// the rather than being stored in instances of the complex class. + /// + /// The type of the property to be configured. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// An object that can be used to configure the property. + public virtual ComplexPropertyBuilder ComplexProperty( + Expression> propertyExpression) + where TProperty : struct + => new( + TypeBuilder.ComplexProperty( + Check.NotNull(propertyExpression).GetMemberAccess(), + complexTypeName: null, + collection: false, + ConfigurationSource.Explicit)!.Metadata); + + /// + /// Returns an object that can be used to configure a complex property of the complex type. + /// If no property with the given name exists, then a new property will be added. + /// + /// + /// When adding a new property, if a property with the same name exists in the complex class + /// then it will be added to the model. If no property exists in the complex class, then + /// a new shadow state complex property will be added. A shadow state property is one that does not have a + /// corresponding property in the complex class. The current value for the property is stored in + /// the rather than being stored in instances of the complex class. + /// + /// The type of the property to be configured. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// The name of the complex type. + /// An object that can be used to configure the property. + public virtual ComplexPropertyBuilder ComplexProperty( + Expression> propertyExpression, + string complexTypeName) + where TProperty : struct + => new( + TypeBuilder.ComplexProperty( + Check.NotNull(propertyExpression).GetMemberAccess(), + Check.NotEmpty(complexTypeName), + collection: false, + ConfigurationSource.Explicit)!.Metadata); + + /// + /// Configures a complex property of the complex type. + /// If no property with the given name exists, then a new property will be added. + /// + /// + /// When adding a new property, if a property with the same name exists in the complex class + /// then it will be added to the model. If no property exists in the complex class, then + /// a new shadow state complex property will be added. A shadow state property is one that does not have a + /// corresponding property in the complex class. The current value for the property is stored in + /// the rather than being stored in instances of the complex class. + /// + /// The type of the property to be configured. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// An action that performs configuration of the property. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexPropertyBuilder ComplexProperty( + Expression> propertyExpression, + Action> buildAction) + where TProperty : struct + { + Check.NotNull(buildAction); + + buildAction(ComplexProperty(propertyExpression)); + + return this; + } + + /// + /// Configures a complex property of the complex type. + /// If no property with the given name exists, then a new property will be added. + /// + /// + /// When adding a new property, if a property with the same name exists in the complex class + /// then it will be added to the model. If no property exists in the complex class, then + /// a new shadow state complex property will be added. A shadow state property is one that does not have a + /// corresponding property in the complex class. The current value for the property is stored in + /// the rather than being stored in instances of the complex class. + /// + /// The type of the property to be configured. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// The name of the complex type. + /// An action that performs configuration of the property. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexPropertyBuilder ComplexProperty( + Expression> propertyExpression, + string complexTypeName, + Action> buildAction) + where TProperty : struct + { + Check.NotNull(buildAction); + + buildAction(ComplexProperty(propertyExpression, complexTypeName)); + + return this; + } + /// /// Configures a complex collection of the complex type. /// If no property with the given name exists, then a new property will be added. @@ -528,6 +646,96 @@ public virtual ComplexPropertyBuilder ComplexCollection( return this; } + /// + /// Returns an object that can be used to configure a complex collection property of the complex type. + /// If the specified property is not already part of the model, it will be added. + /// + /// The element type. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// An object that can be used to configure the complex collection property. + public virtual ComplexCollectionBuilder ComplexCollection( + Expression?>> propertyExpression) + where TElement : struct + => new( + TypeBuilder.ComplexProperty( + Check.NotNull(propertyExpression).GetMemberAccess(), + complexTypeName: null, + collection: true, + ConfigurationSource.Explicit)!.Metadata); + + /// + /// Returns an object that can be used to configure a complex collection property of the complex type. + /// If the specified property is not already part of the model, it will be added. + /// + /// The element type. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// The name of the complex type. + /// An object that can be used to configure the complex collection property. + public virtual ComplexCollectionBuilder ComplexCollection( + Expression?>> propertyExpression, + string complexTypeName) + where TElement : struct + => new( + TypeBuilder.ComplexProperty( + Check.NotNull(propertyExpression).GetMemberAccess(), + Check.NotEmpty(complexTypeName), + collection: true, + ConfigurationSource.Explicit)!.Metadata); + + /// + /// Configures a complex collection property of the complex type. + /// If the specified property is not already part of the model, it will be added. + /// + /// The element type. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// An action that performs configuration of the property. + /// An object that can be used to configure the complex collection property. + public virtual ComplexPropertyBuilder ComplexCollection( + Expression?>> propertyExpression, + Action> buildAction) + where TElement : struct + { + Check.NotNull(buildAction); + + buildAction(ComplexCollection(propertyExpression)); + + return this; + } + + /// + /// Configures a complex collection property of the complex type. + /// If the specified property is not already part of the model, it will be added. + /// + /// The element type. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// The name of the complex type. + /// An action that performs configuration of the property. + /// An object that can be used to configure the complex collection property. + public virtual ComplexPropertyBuilder ComplexCollection( + Expression?>> propertyExpression, + string complexTypeName, + Action> buildAction) + where TElement : struct + { + Check.NotNull(buildAction); + + buildAction(ComplexCollection(propertyExpression, complexTypeName)); + + return this; + } + /// /// Excludes the given property from the complex type. This method is typically used to remove properties /// or navigations from the complex type that were added by convention. diff --git a/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs b/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs index d8852bbeb9a..0fe91bfeed8 100644 --- a/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs +++ b/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs @@ -359,6 +359,94 @@ public virtual EntityTypeBuilder ComplexProperty( return this; } + /// + /// Returns an object that can be used to configure a complex property of the entity type. + /// If the specified property is not already part of the model, it will be added. + /// + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// An object that can be used to configure the complex property. + public virtual ComplexPropertyBuilder ComplexProperty( + Expression> propertyExpression) + where TProperty : struct + => new( + Builder.ComplexProperty( + Check.NotNull(propertyExpression).GetMemberAccess(), + complexTypeName: null, + collection: false, + ConfigurationSource.Explicit)! + .Metadata); + + /// + /// Returns an object that can be used to configure a complex property of the entity type. + /// If the specified property is not already part of the model, it will be added. + /// + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// The name of the complex type. + /// An object that can be used to configure the complex property. + public virtual ComplexPropertyBuilder ComplexProperty( + Expression> propertyExpression, + string complexTypeName) + where TProperty : struct + => new( + Builder.ComplexProperty( + Check.NotNull(propertyExpression).GetMemberAccess(), + Check.NotEmpty(complexTypeName), + collection: false, + ConfigurationSource.Explicit)! + .Metadata); + + /// + /// Configures a complex property of the entity type. + /// If the specified property is not already part of the model, it will be added. + /// + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// An action that performs configuration of the property. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual EntityTypeBuilder ComplexProperty( + Expression> propertyExpression, + Action> buildAction) + where TProperty : struct + { + Check.NotNull(buildAction); + + buildAction(ComplexProperty(propertyExpression)); + + return this; + } + + /// + /// Configures a complex property of the entity type. + /// If the specified property is not already part of the model, it will be added. + /// + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// The name of the complex type. + /// An action that performs configuration of the property. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual EntityTypeBuilder ComplexProperty( + Expression> propertyExpression, + string complexTypeName, + Action> buildAction) + where TProperty : struct + { + Check.NotNull(buildAction); + + buildAction(ComplexProperty(propertyExpression, complexTypeName)); + + return this; + } + /// /// Configures a complex collection of the entity type. /// If no property with the given name exists, then a new property will be added. @@ -548,6 +636,96 @@ public virtual EntityTypeBuilder ComplexCollection( return this; } + /// + /// Returns an object that can be used to configure a complex collection property of the entity type. + /// If the specified property is not already part of the model, it will be added. + /// + /// The element type. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// An object that can be used to configure the complex collection property. + public virtual ComplexCollectionBuilder ComplexCollection( + Expression?>> propertyExpression) + where TElement : struct + => new( + Builder.ComplexProperty( + Check.NotNull(propertyExpression).GetMemberAccess(), + complexTypeName: null, + collection: true, + ConfigurationSource.Explicit)!.Metadata); + + /// + /// Returns an object that can be used to configure a complex collection property of the entity type. + /// If the specified property is not already part of the model, it will be added. + /// + /// The element type. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// The name of the complex type. + /// An object that can be used to configure the complex collection property. + public virtual ComplexCollectionBuilder ComplexCollection( + Expression?>> propertyExpression, + string complexTypeName) + where TElement : struct + => new( + Builder.ComplexProperty( + Check.NotNull(propertyExpression).GetMemberAccess(), + Check.NotEmpty(complexTypeName), + collection: true, + ConfigurationSource.Explicit)!.Metadata); + + /// + /// Configures a complex collection property of the entity type. + /// If the specified property is not already part of the model, it will be added. + /// + /// The element type. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// An action that performs configuration of the property. + /// An object that can be used to configure the complex collection property. + public virtual EntityTypeBuilder ComplexCollection( + Expression?>> propertyExpression, + Action> buildAction) + where TElement : struct + { + Check.NotNull(buildAction); + + buildAction(ComplexCollection(propertyExpression)); + + return this; + } + + /// + /// Configures a complex collection property of the entity type. + /// If the specified property is not already part of the model, it will be added. + /// + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// The element type. + /// The name of the complex type. + /// An action that performs configuration of the property. + /// An object that can be used to configure the complex collection property. + public virtual EntityTypeBuilder ComplexCollection( + Expression?>> propertyExpression, + string complexTypeName, + Action> buildAction) + where TElement : struct + { + Check.NotNull(buildAction); + + buildAction(ComplexCollection(propertyExpression, complexTypeName)); + + return this; + } + /// /// Returns an object that can be used to configure an existing navigation property of the entity type. /// It is an error for the navigation property not to exist. diff --git a/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs b/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs index 9ffd79a04ea..ace8ad64b6a 100644 --- a/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs @@ -1083,7 +1083,7 @@ protected virtual void RemovePropertyIfUnused(Property property, ConfigurationSo if (collection == false) { - complexType = propertyType; + complexType = propertyType.UnwrapNullableType(); } if (collection == null @@ -1091,7 +1091,7 @@ protected virtual void RemovePropertyIfUnused(Property property, ConfigurationSo { var elementType = propertyType.TryGetSequenceType(); collection ??= elementType != null; - complexType ??= collection.Value ? elementType : propertyType; + complexType ??= (collection.Value ? elementType : propertyType)?.UnwrapNullableType(); } foreach (var derivedType in Metadata.GetDerivedTypes()) diff --git a/test/EFCore.Relational.Specification.Tests/ComplexTypesTrackingRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/ComplexTypesTrackingRelationalTestBase.cs new file mode 100644 index 00000000000..c30a62c6688 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/ComplexTypesTrackingRelationalTestBase.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore; + +public abstract class ComplexTypesTrackingRelationalTestBase : ComplexTypesTrackingTestBase + where TFixture : ComplexTypesTrackingRelationalTestBase.RelationalFixtureBase +{ + protected ComplexTypesTrackingRelationalTestBase(TFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + fixture.TestSqlLoggerFactory.Clear(); + fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); + + public abstract class RelationalFixtureBase : FixtureBase + { + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity( + b => + { + b.ComplexCollection( + e => e.Activities, b => + { + b.ToJson(); + }); + }); + + modelBuilder.Entity( + b => + { + b.ComplexCollection( + e => e.Activities, b => + { + b.ToJson(); + }); + }); + + if (!UseProxies) + { + modelBuilder.Entity( + b => + { + b.ComplexCollection( + e => e.Activities, b => + { + b.ToJson(); + }); + }); + + modelBuilder.Entity( + b => + { + b.ComplexCollection( + e => e.Activities, b => + { + b.ToJson(); + }); + }); + } + } + } +} diff --git a/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs b/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs index ced8d9b0946..43e3a666102 100644 --- a/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs +++ b/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs @@ -519,9 +519,7 @@ private async Task TrackAndSaveTest(EntityState state, bool async, Func AssertPropertiesModified(entry, state == EntityState.Modified); } - //TODO: SaveChanges support #31237 - if (!hasCollections - && (state == EntityState.Added || state == EntityState.Unchanged)) + if (state == EntityState.Added || state == EntityState.Unchanged) { _ = async ? await context.SaveChangesAsync() : context.SaveChanges(); @@ -918,6 +916,42 @@ public virtual async Task Throws_only_when_saving_with_null_second_level_complex () => async ? context.SaveChangesAsync() : Task.FromResult(context.SaveChanges()))).Message); } + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Can_save_null_second_level_complex_property_with_required_properties(bool async) + { + using var context = CreateContext(); + + await context.Database.CreateExecutionStrategy().ExecuteAsync( + context, async context => + { + using var transaction = context.Database.BeginTransaction(); + + var yogurt = CreateYogurt(context, nullLicense: true); + var entry = async ? await context.AddAsync(yogurt) : context.Add(yogurt); + + if (async) + { + await context.SaveChangesAsync(); + } + else + { + context.SaveChanges(); + } + + // #31376 + //var actualYogurt = async + // ? await context.Set().OrderBy(y => y.Id).AsNoTracking().FirstAsync() + // : context.Set().OrderBy(y => y.Id).AsNoTracking().First(); + + //Assert.Null(actualYogurt.Culture.License!.Value.Tag); + //Assert.Null(actualYogurt.Culture.Manufacturer.Tag); + //Assert.Null(actualYogurt.Milk.License!.Value.Tag); + //Assert.Null(actualYogurt.Milk.Manufacturer.Tag); + }); + } + [ConditionalTheory] [InlineData(false)] [InlineData(true)] @@ -933,7 +967,22 @@ await context.Database.CreateExecutionStrategy().ExecuteAsync( var yogurt = CreateYogurt(context, nullTag: true); var entry = async ? await context.AddAsync(yogurt) : context.Add(yogurt); - context.SaveChanges(); + if (async) + { + await context.SaveChangesAsync(); + } + else + { + context.SaveChanges(); + } + + // #31376 + //var actualYogurt = async + // ? await context.Set().OrderBy(y => y.Id).AsNoTracking().FirstAsync() + // : context.Set().OrderBy(y => y.Id).AsNoTracking().First(); + + //Assert.Null(actualYogurt.Culture.License); + //Assert.Null(actualYogurt.Milk.License); }); } @@ -3321,7 +3370,7 @@ public struct Culture public int Rating { get; set; } public bool? Validation { get; set; } public Manufacturer Manufacturer { get; set; } - public License License { get; set; } + public License? License { get; set; } } public class Milk @@ -3331,7 +3380,7 @@ public class Milk public int Rating { get; set; } public bool? Validation { get; set; } public Manufacturer Manufacturer { get; set; } = null!; - public License License { get; set; } + public License? License { get; set; } } public class Manufacturer @@ -3360,7 +3409,7 @@ public struct Tog public string? Text { get; set; } } - protected Yogurt CreateYogurt(DbContext context, bool nullMilk = false, bool nullManufacturer = false, bool nullTag = false) + protected Yogurt CreateYogurt(DbContext context, bool nullMilk = false, bool nullManufacturer = false, bool nullTag = false, bool nullLicense = false) { var yogurt = Fixture.UseProxies ? context.CreateProxy() @@ -3370,13 +3419,15 @@ protected Yogurt CreateYogurt(DbContext context, bool nullMilk = false, bool nul yogurt.Culture = new Culture { - License = new License - { - Charge = 1.0m, - Tag = nullTag ? null! : new Tag { Text = "Ta1" }, - Title = "Ti1", - Tog = new Tog { Text = "To1" } - }, + License = nullLicense + ? null + : new License + { + Charge = 1.0m, + Tag = nullTag ? null! : new Tag { Text = "Ta1" }, + Title = "Ti1", + Tog = new Tog { Text = "To1" } + }, Manufacturer = nullManufacturer ? null! : new Manufacturer @@ -3395,13 +3446,15 @@ protected Yogurt CreateYogurt(DbContext context, bool nullMilk = false, bool nul ? null! : new Milk { - License = new License - { - Charge = 1.0m, - Tag = nullTag ? null! : new Tag { Text = "Ta1" }, - Title = "Ti1", - Tog = new Tog { Text = "To1" } - }, + License = nullLicense + ? null + : new License + { + Charge = 1.0m, + Tag = nullTag ? null! : new Tag { Text = "Ta1" }, + Title = "Ti1", + Tog = new Tog { Text = "To1" } + }, Manufacturer = nullManufacturer ? null! : new Manufacturer diff --git a/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs index 9f8b068e382..d9e4670cc5d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs @@ -3,8 +3,6 @@ namespace Microsoft.EntityFrameworkCore; -#nullable disable - public class ComplexTypesTrackingSqlServerTest( ComplexTypesTrackingSqlServerTest.SqlServerFixture fixture, ITestOutputHelper testOutputHelper) @@ -336,25 +334,17 @@ protected override IServiceCollection AddServices(IServiceCollection serviceColl } } -public abstract class ComplexTypesTrackingSqlServerTestBase : ComplexTypesTrackingTestBase +public abstract class ComplexTypesTrackingSqlServerTestBase : ComplexTypesTrackingRelationalTestBase where TFixture : ComplexTypesTrackingSqlServerTestBase.SqlServerFixtureBase, new() { protected ComplexTypesTrackingSqlServerTestBase(TFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) + : base(fixture, testOutputHelper) { - fixture.TestSqlLoggerFactory.Clear(); - fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) - => facade.UseTransaction(transaction.GetDbTransaction()); - - public abstract class SqlServerFixtureBase : FixtureBase + public abstract class SqlServerFixtureBase : RelationalFixtureBase { protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; - - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; } } diff --git a/test/EFCore.Sqlite.FunctionalTests/ComplexTypesTrackingSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/ComplexTypesTrackingSqliteTest.cs index e0f89c85f74..d03f0daeada 100644 --- a/test/EFCore.Sqlite.FunctionalTests/ComplexTypesTrackingSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/ComplexTypesTrackingSqliteTest.cs @@ -3,26 +3,16 @@ namespace Microsoft.EntityFrameworkCore; -#nullable disable - -public class ComplexTypesTrackingSqliteTest : ComplexTypesTrackingTestBase +public class ComplexTypesTrackingSqliteTest : ComplexTypesTrackingRelationalTestBase { public ComplexTypesTrackingSqliteTest(SqliteFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) + : base(fixture, testOutputHelper) { - fixture.TestSqlLoggerFactory.Clear(); - fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } - protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) - => facade.UseTransaction(transaction.GetDbTransaction()); - - public class SqliteFixture : FixtureBase, ITestSqlLoggerFactory + public class SqliteFixture : RelationalFixtureBase, ITestSqlLoggerFactory { protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; - - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; } }