diff --git a/src/EFCore.Design/Migrations/Internal/SnapshotModelProcessor.cs b/src/EFCore.Design/Migrations/Internal/SnapshotModelProcessor.cs index 40bf9f2bb00..2fa73c9a226 100644 --- a/src/EFCore.Design/Migrations/Internal/SnapshotModelProcessor.cs +++ b/src/EFCore.Design/Migrations/Internal/SnapshotModelProcessor.cs @@ -77,6 +77,8 @@ public SnapshotModelProcessor( ProcessElement(element.DependentToPrincipal, version); ProcessElement(element.PrincipalToDependent, version); } + + ProcessComplexProperties(entityType, version); } } @@ -110,6 +112,31 @@ private void ProcessElement(IReadOnlyEntityType entityType, string version) } } + private void ProcessComplexProperties(IReadOnlyTypeBase typeBase, string version) + { + foreach (var complexProperty in typeBase.GetComplexProperties()) + { + ProcessElement(complexProperty, version); + + if (complexProperty is IMutableComplexProperty mutableComplexProperty) + { + UpdateComplexPropertyNullability(mutableComplexProperty, version); + } + + ProcessComplexProperties(complexProperty.ComplexType, version); + } + } + + private static void UpdateComplexPropertyNullability(IMutableComplexProperty complexProperty, string version) + { + if ((version.StartsWith("8.", StringComparison.Ordinal) + || version.StartsWith("9.", StringComparison.Ordinal)) + && !complexProperty.ClrType.IsNullableType()) + { + complexProperty.IsNullable = false; + } + } + private void ProcessElement(IReadOnlyAnnotatable? metadata, string version) { if (version.StartsWith("1.", StringComparison.Ordinal) diff --git a/src/ef/Commands/DbContextOptimizeCommand.cs b/src/ef/Commands/DbContextOptimizeCommand.cs index 9bb7a18ee58..48eb6e5c038 100644 --- a/src/ef/Commands/DbContextOptimizeCommand.cs +++ b/src/ef/Commands/DbContextOptimizeCommand.cs @@ -53,7 +53,10 @@ protected override int Execute(string[] args) _precompileQueries!.HasValue(), _nativeAot!.HasValue()); - ReportResults(result); + if (result != null) + { + ReportResults(result); + } return base.Execute(args); } diff --git a/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs index 0b213ad4f20..5faa18ad8ca 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/SnapshotModelProcessorTest.cs @@ -201,6 +201,98 @@ public void Can_diff_against_older_sequence_model(Type snapshotType) AssertSameSnapshot(snapshotType, context); } + [ConditionalFact] + public void Updates_complex_property_nullability_for_pre_10_snapshots() + { + var builder = new ModelBuilder(); + var model = builder.Model; + ((Model)model).SetProductVersion("9.0.0"); + + var entityType = builder.Entity(); + entityType.ComplexProperty(e => e.StructComplexProperty, b => + { + b.Property(c => c.Value); + }); + + var complexProperty = entityType.Metadata.GetComplexProperties().Single(); + Assert.Equal(typeof(StructComplexType), complexProperty.ClrType); + + var complexPropertyInternal = (ComplexProperty)complexProperty; + Assert.Null(complexPropertyInternal.GetIsNullableConfigurationSource()); + Assert.False(complexProperty.IsNullable); + + var reporter = new TestOperationReporter(); + var processor = new SnapshotModelProcessor(reporter, DummyModelRuntimeInitializer.Instance); + processor.Process(model); + + Assert.NotNull(complexPropertyInternal.GetIsNullableConfigurationSource()); + Assert.False(complexProperty.IsNullable); + Assert.Empty(reporter.Messages); + } + + [ConditionalFact] + public void Does_not_update_complex_property_nullability_for_10_or_later_snapshots() + { + var builder = new ModelBuilder(); + var model = builder.Model; + ((Model)model).SetProductVersion("10.0.0"); + + var entityType = builder.Entity(); + entityType.ComplexProperty(e => e.StructComplexProperty, b => + { + b.Property(c => c.Value); + }); + + var complexProperty = entityType.Metadata.GetComplexProperties().Single(); + var complexPropertyInternal = (ComplexProperty)complexProperty; + + Assert.Null(complexPropertyInternal.GetIsNullableConfigurationSource()); + + var reporter = new TestOperationReporter(); + var processor = new SnapshotModelProcessor(reporter, DummyModelRuntimeInitializer.Instance); + processor.Process(model); + + Assert.Null(complexPropertyInternal.GetIsNullableConfigurationSource()); + Assert.Empty(reporter.Messages); + } + + [ConditionalFact] + public void Updates_nested_complex_property_nullability_for_pre_10_snapshots() + { + var builder = new ModelBuilder(); + var model = builder.Model; + ((Model)model).SetProductVersion("9.0.0"); + + var entityType = builder.Entity(); + entityType.ComplexProperty(e => e.OuterComplexProperty, b => + { + b.Property(c => c.Value); + b.ComplexProperty(c => c.InnerComplexProperty, b2 => + { + b2.Property(c2 => c2.Value); + }); + }); + + var outerComplexProperty = entityType.Metadata.GetComplexProperties().Single(); + var innerComplexProperty = outerComplexProperty.ComplexType.GetComplexProperties().Single(); + + var outerComplexPropertyInternal = (ComplexProperty)outerComplexProperty; + var innerComplexPropertyInternal = (ComplexProperty)innerComplexProperty; + + Assert.Null(outerComplexPropertyInternal.GetIsNullableConfigurationSource()); + Assert.Null(innerComplexPropertyInternal.GetIsNullableConfigurationSource()); + + var reporter = new TestOperationReporter(); + var processor = new SnapshotModelProcessor(reporter, DummyModelRuntimeInitializer.Instance); + processor.Process(model); + + Assert.NotNull(outerComplexPropertyInternal.GetIsNullableConfigurationSource()); + Assert.False(outerComplexProperty.IsNullable); + Assert.NotNull(innerComplexPropertyInternal.GetIsNullableConfigurationSource()); + Assert.False(innerComplexProperty.IsNullable); + Assert.Empty(reporter.Messages); + } + private static void AssertSameSnapshot(Type snapshotType, DbContext context) { var differ = context.GetService(); @@ -363,6 +455,34 @@ private class BlogDetails public ICollection Posts { get; set; } } + private class EntityWithComplexProperty + { + public int Id { get; set; } + public StructComplexType StructComplexProperty { get; set; } + } + + private struct StructComplexType + { + public int Value { get; set; } + } + + private class EntityWithNestedComplexProperty + { + public int Id { get; set; } + public OuterStructComplexType OuterComplexProperty { get; set; } + } + + private struct OuterStructComplexType + { + public int Value { get; set; } + public InnerStructComplexType InnerComplexProperty { get; set; } + } + + private struct InnerStructComplexType + { + public int Value { get; set; } + } + private class OwnershipModelSnapshot2_0 : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder)